• c++ 学习笔记


    C++

    更新于20200320 15:17
    

    引用与指针的区别

    1. 指针有多级,引用没有多级
    2. 指针可以为null,但是引用不可以
    3. 在加法运算符的方面
        int *p;
        int &q;
        p ++;
        q ++;
    

    两者概念是不同的,p++指的是地址的增加,q++指的是引用的值的增加
    4. 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变

        string s1("hello");
        string s2("world");
        string& rs = s1;
        string *ps = &s1;
        rs = s2;//惊了,现在s1变成了world
        cout << s1 << endl;
        ps = &s2;
    
    1. 指针可以不初始化,引用必须被初始化
    int &p//declared as reference but not initialized
    
    1. 比如在实现operator[]这个运算符的时候,可以使用reference来实现(goto 后文的bitwise const绕开问题,具体就是operator[]返回对应的&,然后这样并不违反const function的bitwise const原则,你返回的reference可以被修改)

    关键词 explicit

    explicit解决隐式类型转换带来的问题
    举个例子

    template<class T>
    class Array {
    public:
        Array(int size);
        T& operator[](int index);
    }
    bool operator== (cosnt Array<int> &a, const Array<int> &b);
    Array<int>a1(10), b1(10);
    for(int i = 0; i < 10; i ++){
        if(a1 == b1[i]){//会被隐式转换为a1 == static_cast< Array<int> >(b[i])
            //do something
        }
    }
    

    ++ 与 -- 前置与后置

    1. 重载函数以其参数类型区分彼此,后置型的重载与前置型的重载区别在于,后置的多个变量(int)
    operator++()
    operator++(int)//后置型
    operator--()
    operator--(int)//后置型
    //举个例子
    UPInt& UPInt::operator++(){
        *this += 1;  //累加
        return *this;//取出
    }
    const UPInt UPInt::operator++(int){
        UPInt oldValue = *this;//取出
        ++(*this);             //累加
        return oldValue;
    }
    UPInt i;
    i ++++;//这样的话i只加1次,直觉上肯定是错的
    //等价为i.operator++(0).operator++(0)
    
    
    1. 前置型返回references,后置型返回const

    c++ 四种转型操作符

    1. static_cast: int->double,也限制了一些奇奇怪怪的操作,比如将struct->int,double->pointer,也不能移除表达式的常量性
      举个例子:
    int a = 1.0
    static_cast<double>(a);
    
    1. const_cast:改变表达式中的常量性(constness)或变易性(volatileness),如果用于其他用途,其他转型动作会被拒绝
    void update(SpecialWidget *psw);
    SpecialWidget sw;
    const SpecialWidget& csw = sw;
    update(const_cast<SpecialWidget*>(&csw));
    

    const_cast无法使用cast down(继承体系的向下转型动作)
    3. dynamic_cast:用来执行继承体系中“安全的向下转型或跨系转型动作”,利用dynamic_cast将“指向base class objects的pointers或references"转为"derived class objects的pointers或references",如果转型失败,pointers对应的是null,reference对应的就是exception(异常)

    void update(SpecialWidget *psw);
    widget *pw;
    update(dynamic_cast<SpecialWidge*>(pw));//如果转型成功,会返回一个对应的指针,如果转型失败那么会返回一个对应的null指针
    void updateViaRef(Specualwidget &rsw);
    updateViaRef(dynamic_cast<SpecialWidge&>(*pw));//如果转型成功,会对应返回一个reference,如果失败的话,会返回一个对应的exception,即异常
    
    1. reinterpret_cast:不具有移植性,最常用的用途是“指针的转换”

    确定对象被使用前先被初始化

    如果构造函数没有给出对应的初值,那么他会调用default函数来进行初始赋值
    即进行一次default + copy assignment 单只调用一次copy进行构造 是较为高效的是这样的话
    const
    const或者references进行修饰的变量,就一定需要初值,因为不允许进行赋值

    成员初始化次序是固定的,从base classes更遭遇derived classes被初始化,class成员变量的初始化顺序
    取决于声明的顺序

    对“定义于不同的编译单元内的non-local static对象”的初始化次序并无明确定义,解决这个问题是非常困难的
    对于这种方法 我们可以使用reference-returning,初始化一个local static对象,在第二行返回他,来解决初始化
    次序并无明确定义的问题
    感觉这里讲得太抽象了,举个例子

    class FileSystem{...};
    FileSystem & tfs(){
        static FileSystem fs;//定义并初始化一个static对象
        return fs;
    }
    

    重载&& || ,

    1. c++ 与 c 处理真假表达式采用“鄹死式”评估方法,如果重载后那么“函数调用式语义”会代替原来的
    2. ,也可以被重载,那么,左边会被先评估,右边为返回的值
      你可能想支持这样的性质,但是你无法保证左侧表达式一定比右侧表达式更早被评估
      不能重载的运算符如下:
      . .* :: ?: new delete sizeof typeid 四个转换运算符

    const与#define

    1. 编译器处理的方法不同,#define是在预处理阶段展开的,const常量是在编译运行阶段使用的,《effective c++》中的说法是#define不被视为语言的一部分
      举个例子:
    #define a 1.653
    //有可能存在a没进入记号表(symbol table),但是你使用了a,那么编译器就会报错,错误消息不会提示是a错误,而是提示1.653,然后你debug就会相对困难
    
    1. 类的安全性检查,#define没有类型检查,仅仅是展开而已,const有编译类型,在编译时会进行检查。
    2. #define不会分配内存,define宏仅仅是展开,有多少个地方就展开多少次,const定义时,会在内存中分配,《effective c++》中的说法是使用常量可能比使用#define导致更小的码
    3. 赋值时的空间分配,const可以减少空间,避免不必要的空间分配,
    #define PI 3.14159 //常量宏  
    const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......  
    double i=Pi; //此时为Pi分配内存,以后不再分配!  
    double I=PI; //编译期间进行宏替换,分配内存  
    double j=Pi; //没有内存分配  
    double J=PI; //再进行宏替换,又一次分配内存!  
    

    const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。
    5. 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
    6. 在类中 #define是不可以被封装的,const可以被封装,也就是说没有所谓的private #define
    7. 取一个const空间是合法的,取一个#define的空间一般是不合法的
    8. 如果使用#define来实现宏,像函数但是不会招致函数调用带来的额外开销,但是宏会出现一些问题,对于形似函数的宏,最好使用inline函数来替换#define ---《effective c++》

    使用宏定义求结构体成员偏移量

    #define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE*)0) -> MEMBER)
    /*
    (TYPE*)0 将0转型成TYPE类型指针
    ((TYPE*)0->MEMBER) 访问结构体中的成员
    &((TYPE*)0->MEMBER) 取出数据成员地址,也就是相对于0的偏移量
    (size_t) &((TYPE*)0->MEMBER) 将结果转成size_t类型
    */
    

    new与malloc的区别

    1. 申请的内存所在位置不同,malloc是从自由存储区上分配内存,new是从堆分配内存
    2. 返回内存的安全性,new返回的是对象类型的指针, malloc返回的是void*,需要通过强制类型转换来进行转换为我们需要的类型,new是类型安全的操作符,类型安全的代码不会试图调用未授权的内存空间
    3. 内存失败分配的返回值,new内存失败返回的是bac_alloc,不会返回null;而malloc返回NULL
    4. 是否需要指定内存大小,使用new操作的话无需指定内存块的大小,而malloc则需要显式的指定内存的大小
    5. new/delete会调用对象的构造/析构函数,malloc不会
    6. 对数组的处理,new[]/delete[]配套使用,malloc则没有专门对于数组的处理
    7. new/delete的实现可以基于malloc/free,而malloc的实现不可以去调用new
    8. new/delete可以被重载,malloc/free不允许重载
    9. malloc在运行过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。new则不行
    10. 在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。
      对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

    new 的不同意义

    string *ps = new string("dieowjoe");
    
    1. 实际上使用的是new operator 这个操作符是语言内建的,是不可以被重载的(1)他分配足够的内存来放置对象(2)他调用对象的构造函数constructor
    2. operator new 函数声明通常如下
    void * operator new(size_t size);
    //你可以以任意的形式来重载operator new,但是得保证第一个参数为size_t
    

    3.从第一点看过来的具体实现

    void *memory = operator new(sizeof(string));
    call string::string("dieowjoe") on *memory;//然而这一步设计到调用一个constructor
                                               //作为一个程序员我居然没有权利调用这一步,干
    string *ps = static_cast<string*>(memory);
    
    1. Placement new,这东西可以再分配好的原始内存上构建对象
    new (buffer) Widget(WidgetSize)//more effective c++ 条款8
    //作为new operator的隐式调用之一
    //这一点,笔记没有做好,想详细了解可以自行看 more effective c++ 条款8
    
    1. 针对数组的new数组的内存分配工作,不再由operator new来负责,而是由一个数组版的函数operator new[]来负责
      而new operator针对array版本相对的会为每个对象调用一次constructor,默认调用default constructor

    delete 的不同意义

    1. operator delete跟delete operator类比于operator new跟new operator
    delete ps;
    //等同于下面
    ps->~strong();
    operator delete(ps);
    
    1. 使用operator new获得的内存应该是用operator delete来进行回收,不可以使用new operator 与 delete operator
      例如如下
    void *buffer = operator new(50 * sizeof(char));
    operator delete(buffer);
    //这种行为相当于在c中调用malloc free
    
    1. 使用placement new不应该使用operator delete或者delete operator来进行回收
    void freeShared(void *memory);
    pw ->~widget();
    freeShared(pw);
    //具体可以参考more effectie c++ 条款8
    
    1. operator delete[]释放内存,会调用destructor

    const

    1. 变量
    2. 指针(顶层:声明常量指针 char *const p; 底层:声明指向常量的指针 const char *p),const出现在*号的左边,表示被指物为常量,如果出现在*号的右边那么指指针为常量,如果出现在*号的两边那么指的是被指物与指针都是常量
    3. 函数参数(引用、指针)
      例子
    void f1(const weight * p)
    void f1(weight const * p)
    

    这两种写法都是相同的
    4. 函数返回值(常量)
    5. 成员函数(不能修改成员变量)
    const 放在成员函数的前面,指的是返回值为const
    const 放在成员函数的后面,指的是隐藏*this指针(遵守bitwise const原则)

    两种说法:

    1. 成员函数只有在不改变对象之任何成员变量时才可以说是const (bitwise const)
      bitwise存在可以绕开编译的问题,
      举例
    class CTextBlock{
    public:
        char & operator[](std::size_t position) const//bitwise const 声明
        {return pText[position];}
    private:
        char * pText;
    };
    const CTextBlock cctb("hello");
    char * c = &cctb[0];
    *c = 'J';
    

    2.一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此
    使用const成员函数遵守bitwise const声明,当你使用const函数想声明变量,那么该如何操作呢?
    使用对用的命名mutable(可变的)利用mutable释放掉non-static成员变量的bitwise constness约束
    const成员函数可用来处理取得的const对象
    non-const operator[]调用自己的兄弟cosnt operator[]这样导致递归调用自己
    如何在const函数修改成员变量的值?

    • 使用mutable关键字
    • 造一个假的this去操作成员变量
      void Class1::func1() const {
          //声明一个指针指向this所指对象,并先将这个对象的常量性转型成const
          Class1 * const fakeClass1 = const_cast<Class1* const>(this);
          //使用造出来的const指针,去修改成员变量
          fakeClass1->_value = 1;
      }
      

    default constructor(默认构造函数)

    存在的意义:“合理的无中生有对象”

    1. 如果没有default constructor 那么产生数组的话,会失败。
      存在两种解决方案:1.提供必要的参数。2.使用指针数组指向
      没有default constructor的class容易造成的问题是 template可能会造成问题 (具体参照more effective c++ 条款4)
    2. Virtual base class如果没有default constructor那么他的derived class objects都必须了解virtual base class的变量含义,不然会发生错误

    static

    1. 全局变量函数(隐藏,本文件可见)
      同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
    2. 局部变量(保持变量内容的持久,函数调用)
      static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)
      存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
      共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见
      PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
    3. static的第三个作用是默认初始化为0(static变量)
    4. 成员函数(没有this指针,无法访问成员变量)
    5. 成员变量(属于类的)
    6. static 类对象必须要在类外进行初始化
    7. static 修饰的变量先于对象存在,static变量要在类外进行初始化

    模板全特化和偏特化

    • 全特化:全特化就是限定死模板实现的具体类型
    • 偏特化(局部特化):偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。

    多态

    多态如何实现,内存布局

    1. 继承的最重要的性质之一:可以通过“指向base class objects”的pointers或references,来操作derived class objects,我们可以称这样的pointers或references的行为是多态的。
      (1)使用derived class objects的pointers或者references来操作数组的话,会造成下述情况
      一般不使用多态的方式来处理数组,因为arr[i]其实是一个“指针算数表达式”的简写,这么说吧,arr[i] == (arr + i)
      本质是arr+sizeof()
      i,那么因为多态的话,使用derived class objects来传递的话,那么就会出现按照arr+sizeof(base class objects)*i,那么不能得到正确的答案了
      (2)使用base class objects操作对应来delete[] derived class objects,这种情况主要是因为base class objects的析构函数不是virtual function
      多态分为两种:编译时多态运行时多态
    • 编译时多态
      编译时多态由函数重载,以及模板实现
    • 运行时多态
      运行时多态由虚指针和虚表实现

    了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本(more effective c++条款24)

    1. 当一个虚函数被调用的时候,执行的代码必须对应于“调用者的动态类型”。对象的point或reference,其类型是无形的,那么
      编译器如何很有效率的提供这样的行为
      大部分编译器使用virtual tablesvirtual table pointers一般简写为vtbls vptrs
      vtbls通常是一个由“函数指针”架构而成的数组,某些编译器会以链表的形式来取代数组
      程序中每一个class声明或者继承虚函数者都有自己的一个virtual tables,其中的条目就是该class的各个虚函数
      实现体的指针
      讲道理这边应该得有个举例的图,但是因为我懒,所以建议自己去看,More effecttive c++ 条款24
      使用虚函数的第一个成本:你必须为每个拥有vitual function的class耗费一个vtbl的空间
    2. 编译器如何知道自己应该产生哪些vtbls
      (1)暴力式做法,在每个需要vtbl的目标文件内都产生一个vtbl副本,最后由连接器剔除重复的副本,使得最终的可执行文件或程序库内,只留下每个vtbl的单一实体
      (2)探勘式做法,决定哪一个目标文件应该内含某个class的vtbl。做法如下:class's vtbl被产生在第一个non-inline,non-pure-virtual-function的目标文件中。因此,先前class C1的vtbl应该放在内含C1::~C1定义的目标文件中
      如果使用虚函数使用了inline,会存在麻烦,基于探勘式做法的编译器,会在每一个“使用了class's vtbl”的目标文件中产生
      一个vtbl复制品,使用的内存空间就会变大(通常编译器都会忽略虚函数的inline请求)
      如果使用虚函数使用了inline,因为“inline”意味“在编译期,将调用端的调用动作才被调用函数的函数本体取代”,而“virtual”则意味着“等待,直到运行时期才知道哪个函数被调用了”所以编译器面对调用动作的时候,无法知道哪个函数
      该被调用。
      PS:如果虚函数通过对象被调用,那么可以通过inlined优化,但大部分虚函数调用动作是通过对象的point或者reference
      完成,此类行为无法被inlined,由于此等行为是常态,所以虚函数事实上等于无法被inlined。
    3. 凡是声明有virtual function的class,其对象都有一个隐藏的data member,用来指向该表的vtbl。当然这个所谓的data member就是我们所谓的vptr,你必须在每一个拥有虚函数的对象内付出“一个额外指针”的代价。
      那么怎样找到对应的虚函数调用呢?
      (1)根据对象的vptr找到对应的vtbl。
      (2)找到被调用的函数在vtbl内的对应指针
      (3)调用步骤2所取得的指针所指向的函数
      假设个例子来看看
    pC1->f1();
    (*pc->vptr[i])(pC1);
    //调用pC1->vptr所指向的vtbl中的第i个条目所指函数,将pC1传给改函数作为"this"指针之用
    

    恐怖的多重继承菱形图

    在non-virtual base class情况下,如果derived class在其base class有多条继承路径,则base class的data
    member 有多条继承路径,则base class的data members会在每一个devied class object体内复制,让base class
    变成对应的virtual,可以消除复制现象

    class A{};
    class B: virtual public A{};
    class C: virtual public A{};
    class D: public B, public C{};
    

    有图记得参考书哦P119
    提一点:virtual base classes可能会导致对象里的隐藏指针的增加

    补充一些关于菱形继承的内容

    从上述对应的举例 我们容易得知 D多继承了B C这两个类 这样会导致对应的D中会有A的两份复制
    如果你使用对应的A中的某个变量 那么后果就是导致了报错 这就是所谓的菱形继承的二义性
    当然存在第一种解决的方案 使用定义域举个例子 如下

    D.B::a_data = ....;//类似于这样的实现
    

    当然也有第二种解决方案 即使用对应的虚继承
    虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。虚基类用virtual声明继承关系就行了。这样一来,D就只有A的一份拷贝。

    运行时期类型辨识(RTTI)

    RTTI可以在运行时期或得objects和classes的相关信息,所以一定得有地方存放那些消息
    它们被存放在类型为type_info的对象里,你可以利用typeid操作符取得某个class对应的
    type_info对象
    RTTI设计理论是根据class的vtbl来实现的,只要在vtbls增加一个条目type_info对象空间
    具体如书,书上有示意图
    ps:这个可以拓展一点RTTI的使用,然而我太懒了

    将constructor跟non-member function虚化(more effective c++ 条款25)

    当然第一次面对"virtual constructor",肯定没什么道理,确实,我们什么情况下使用对象的virtual function
    当你手上有一个对象的point或reference,你不知道该对象的真正类型是什么的时候,你才会调用virtual function
    完成“因类型而异的行为”。当你没有获得对象,但是已经知道需要什么类型的时候,才会调用constructor来构造对象
    这个virtual constructor就不太科学

    1. 但是还真的有这个东西,你且往下看
      举例
    class NLComponent{
    public:
        //其中内含至少一个纯虚函数
    };
    class TextBlock: public NLComponent{
    public:
        //不含任何pure virtual function
    };
    class Graphic: public NLComponent{
    public:
        //不含任何pure virtual function
    }
    class NewsLetter{
    public:
        NewsLetter(istream & str);
    private:
        list<NLComponent*> components;
        static NLComponent * readComponent(istream & str);//<2>
    }
    //<1>
    NewsLetter::NewsLetter(istream & str){
        while(str){
    
        }
    }
    //<2>
    NewsLetter::NewsLetter(istream & str){
        while(str){
            components.push_back(readComponent(str));
        }
    }
    /*
    存在一种情况,可能NewsLetter对象尚未开始运作的时候,可能存储在磁盘中,为了能够根据磁盘上的
    数据产出一份NewsLetter,如果我们让NewsLetter拥有一个constructor并用istream作为自变量
    这个constructor可以从steam读取数据以便于产生必要的核心数据结构如<1>
    或者,如果将棘手的东西移动到另一个名叫readComponent的函数,就变成这样,如<2>
    思考一下,readComponent产生了一个崭新的对象,或许是个TextBlock或者是个Graphic,因为他产生对象
    所以行为仿若constructor,但他能产生不同类型的对象,所以我们说他是一个virtual constructor
    所谓virtual constructor是某种函数,视其获得的输入,可产生不同类型的对象,Virtual constructor在许多
    情况下有用,其中之一就是从磁盘判读取对象信息
    */
    
    1. 有一种特别的virtual constructor所谓的virtual copy constructor,virtual copy constructor会返回一个指针,
      指向其调用者的一个新副本,基于这种类型,virtual copy construc通常用copyself或cloneself命名
      举个例子
    class NLComponent{
    public:
        virtual NLComponent * clone() const = 0;
    }
    class TextBlock:public NLComponent{
    public:
        vitual TextBlock* clone() const{
            return new TextBlock(*this);
        }
    }
    class Graphic: public NLComponent{
    public: 
        vitual Graphic* clone() const{
            return new Graphic(*this);
        }
    }
    

    vitual copy constructor只是调用真正的copy constructor而已
    当你NLComponent拥有一个virtual copy constructor,我们可以为NewLetter实现一个copy constructor

    NewsLetter::NewsLetter(const NewsLetter &rhs){
        for (auto it = rhs.componets.begins(); it != rhs.componets.end(); ++ it){
            components.push_back((*it)->clone());
        }
    }
    
    1. non-member function虚化
      就像constructor无法真正被虚化一样,non-member function也是,然而我们认为应该让non-member function针对不同的对象而操作方式不同,明显的方案1让out操作符operator<<虚化
    class NLComponent{
    public:
    virtual ostream& operator<<(ostream& str) const = 0;
    };
    class TextBlock:public NLComponent{
    public:
    virtual ostream& operator<<(ostream& str) const;
    }
    class Graphic: public NLComponent{
    public: 
    virtual ostream& operator<<(ostream& str) const;
    }
    //但是输出的语法就发生了变化
    TextBlock t;
    Graphic g;
    t << cout;
    g << cout;
    

    我们真正需要的是一个名为operator<<的non-member function 展现出类似print虚函数一般的行为
    这一段“需求描述”其实已经非常接近其“做法描述”,解决方法

    class NLComponent{
    public:
    virtual ostream& print(ostream& str) const = 0;
    };
    class TextBlock:public NLComponent{
    public:
    virtual ostream& print(ostream& str) const;
    }
    class Graphic: public NLComponent{
    public: 
    virtual ostream& print(ostream& str) const;
    }
    inline ostream& operator<<(ostream &s, const NLComponent &c){
        return c.print(s);
    }
    

    non-member functions的虚化十分容易,写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数
    你还可以将非虚函数inline一下,提高效率

    限制某个class所能产生的对象数量(more effective c++的条款26)

    1. 每次产生一个对象,那么就会有个constructor函数被调用,当然阻止对象被使用的方法就是讲constructor声明为private
    class Printer{
    public:
        void submitJob(const PrintJob& job);
        void reset();
        void performSelfTest();
        friend Printer& thePrinter();
    private:
        Printer();
        Printer(const Printer& rhs);
    }
    Printer& thePrinter(){
        static Printer p;
        return p;
    }
    string buffer;
    thePrinter().reset();
    thePrinter().submitJob(buffer);
    

    上述设计有三个成分
    第一,Printer class的constructor属性private,可以压制对象的产生
    第二,全局函数thePrinter被声明为此class的一个friend,让thePrinter
    不受private constructor的约束
    第三,thePrinter内含一个static Printer对象,意思是只有一个Printer对象
    会被产生出来
    差不多的解决方案,你可以将function弄成static member-function
    或者是讲class放入namespace具体more effective c++的条款26
    最好不要将function中的static inlined,因为inline的定义时编译器应该将
    每个调用以函数的本身取代,如果是对于non-member functions 它还意味着其他
    一些事情。他意味着这个函数有内部连接
    如果你有一个inline non-member function并有一个local static,你的程序可能
    会拥有多份该static对象的副本
    其他具体细节请看书,懒得写emmmm
    2. 针对限制产生多个对象的方案,如下

    class Printer{
    public:
        class TooManyObjects{};
        static Printer * makePrinter();
        static Printer * makePrinter(const Printer & rhs);
    private:
        static size_t numObjects;
        static const size_t maxObjects = 10;
        Printer();
        Printer(const Printer &rhs);
    }
    size_t Printer::numObjects = 0;
    const size_t Printer::maxObjects;
    Printer::Printer(){
        if(numObjects >= maxObjects){
            throw TooManyObjects();
        }
    }
    Printer::Printer(const Printer &rhs){
        if(numObjects >= maxObjects){
            throw TooManyObjects();
        }
    }
    static Printer * makePrinter(){
        return new Printer;
    }
    static Printer * makePrinter(const Printer & rhs){
        return new Printer(rhs);
    }
    //接下来书上讲了一个设计base class作为引用计数的方案 具体参照一下书
    

    要求对象产生在heap中(more effective c++ 条款27)

    把对象生成在heap中的方案,显然把constructor跟deconstructor都为private,但是这个方案实在太过
    没有理由让他们都成为private,比较好的方法是让deconstructor为private,而constructor仍为public
    constructor或者deconstructor声明为private将会影响继承跟内含
    PS:吐槽一下怎么会有人想知道自己的类在heap还是stack上啊
    附上对应的代码

    class UPNumber{
    public:
        class HeapConstraintViolation{};
        static void * operator new(size_t size);
        UPNumber();
    private:
        static bool onTheHeap;
    }
    bool UPNumber::operator new(size_t size){
        onTheHeap = true;
        return ::operator new(size);
    }
    UPNumber::UPNumber(){
        if(!onTheHeap){
            throw HeapConstraintViolation();
        }
        onTheHeap = false;
    }
    

    但是会存在一定的问题,比如数组内存分配由operator new[]而非operator new分配
    就算处理了,但是调用operator new[]只调用一次,所以第二次的时候你就又抛出异常了
    根据堆栈的原理,我们可能会写出这样的代码

    bool onHeap(const void *address){
        char onTheStack;
        return address < onTheStack;
        //显然onthestack是当前stack最低的一个,那么我们只要考虑address如果小于onTheStack
        //那么就是位于heap,但是还不够完善,static,反正的地方不是stack也不是heap
        //就算里处理完了这个问题,但是你会发现不同的环境处理可能是不一样的
        //这个时候 你的程序就是不具备移植的效果
    }
    

    禁止对象产生在heap中
    当然对象产生在heap中,显然是使用了系统给的new operator本质上还是调用了operator new,那么
    当然我们不能修改new operator,但是我们可以自定义operator new,比如把类的operator new声明为
    private
    当然在使用了这种情况下,在作为base class那么会出现以下的问题,如下

    class UPNnumber{};
    class NonNegativeUPNumber: public UPNumber{
    };
    NonNegativeUPNumber n1;//ac
    static NonNegativeUPNumber n2;//ac
    NonNegativeUPNumber * p = new NonNegativeUPNumber;//wa,企图调用private operator new
    

    解决方案:在NonNegativeUPNumber中把operator new写到public中

    Smart Pointers(智能指针)

    template<class T>
    class SmartPtr{
    public:
        SmartPtr(T* realPtr = 0);
        SmartPtr(const SmartPtr& rhs);
        ~SmartPtr();
        SmartPtr & operator=(const SmartPtr & rhs);
        T* operator->() const;
        T& operator*() const;
    private:
        T *pointee;
    }
    

    基础构造如上

    1. Smart Pointers的构造、赋值、析构
      参考auto_ptr template
      auto_ptr实现可能如下
    template<class T>
    class auto_ptr{
    public:
        auto_ptr(T* ptr = 0): pointee(ptr){
        }
        ~auto_ptr(){delete pointee;}
    private:
        T* pointeee;
    }
    

    假设同一个对象只能被auto_ptr拥有的前提下,一旦有auto_ptr被复制或被赋值

    auto_ptr<TreeNode>ptn1(new TreeNode);
    auto_ptr<TreeNode>ptn2 = ptn1;//copy constructor
    auto_ptr<TreeNode>ptn3;
    ptn3 = ptn2;
    

    如果我们只是复制其中的dumb pointer 会导致两个auto_ptrs指向同一对象,就会导致一个对象被删除两次
    当然存在两种解决方案,一种是将其的constructor跟deconstructor声明为private,另一种就是采用“对象拥有权”
    的转移 如下

    template<class T>
    class auto_ptr{
    public:
        auto_ptr(auto_ptr<T> &rhs);
        auto_ptr<T>& operator=(auto_ptr<T> & rhs);
    }
    template<class T>
    auto_ptr<T>::auto_ptr(auto_ptr<T> &rhs){
        pointee = rhs.pointee;
        rhs.pointee = null;
    }
    template<class T>
    auto_ptr<T>& operator=(auto_ptr<T> & rhs){
        if(this == &rhs)    return *this;
        delete pointee;
        pointee = rhs.pointee;
        rhs.pointee = 0;
        return *this;
    }
    

    当然对象所有权的转移,会存在问题,比如你通过by value传递,那么你移动到函数中的静态
    变量,等函数结束后,当然被销毁了,那你原来的指针指向就是null了
    所以在调用函数的传递的使用使用by reference-to-const
    2. 实现解引用符

    template <class T>
    T& SmartPtr<T>::operator*() const{
        return *pointee;
    }
    
    1. 实现->符
    template<class T>
    T* SmartPtr<T>::operator->() const{
        return pointee;
    }
    
    1. 测试Smart Pointer是否为Null
    SmartPtr<TreeNode> ptn;
    if(ptn == 0)
    if(ptn)
    if(!ptn)//上述都是错误的
    //你可以这样做
    template<class T>
    class SmartPtr{
    public:
        operator void*();
    }
    //现在上述的做法就可以
    //这样会存在问题,具体看书
    
    1. 将smart pointer转换为Dumb pointer
      可以通过下述方法
    template<class T>
    class DBPtr{
    public:
        operator T*() {
            return pointee;
        }
    };
    

    如果使用上述的方式,必然会导致引用计数的问题,具体参考书
    如果smart pointer classes如果提供隐式转换至dumb point,边打开一坨shit

    DBPtr<Tuple> pt = new Tuple;
    delete pt;
    //如果支持 那么会强行转换为dumb point
    //转换成dumb point就删除了
    

    堆和栈的区别

    stack高地址往低地址成长
    heap低地址往高地址成长

    内联函数和宏定义函数的区别

    1. 运行方面:#define生成的宏定义函数不可以被调试,inline的函数在运行时可以调试
    2. 编译器会对内联函数的参数类型做安全监测或自动类型转换,而宏定义则不会; (安全性方面)
    3. inline的内联函数可以访问类的成员变量,#define生成的宏定义不能访问类的成员变量;
    4. 在类中声明同时定义的成员函数,自动转化为内联函数。

    C++ 的“函数内联”是如何工作的。

    对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,
    那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。
    C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在C++ 程序中,应该用内联函数取代所有宏代码,“断言assert”恐怕是唯一的例外。assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。如果assert是函数,由于函数调用会引起内存、代码的变动,那么将导致Debug版本与Release版本存在差异。所以assert不是函数,而是宏。

    内存泄露以及如何解决

    使用智能指针管理资源
    RAII(Resource Acquisition Is Initialization)资源获取就是初始化

    利用destructors避免泄露资源

    合理的想法是使用一个abstract base class,再派生出对应的concrete classes,由虚函数来负责对不同种类的必要处理动作
    举个例子:

    void processAdoptions(istream& dataSource){
        while(dataSource){
            ALA *pa = readALA(dataSource);
            pa->processAdoption();
            delete pa;
        }
    }
    //假设pa->processAdoption抛出异常exception,那么就意味着接下来的语句都会被跳过,就会内存泄露
    //那么可以使用下面的方式来进行修改
    void processAdoptions(istream& dataSource){
        while(dataSource){
            ALA *pa = readALA(dataSource);
            try{
            pa->processAdoption();
            }
            catch(){
                delete pa;
                throw;
            }
            delete pa;
        }
    }
    //当然是又长又臭 烦死了
    //当然我么使用智能指针就可以了,引用计数归0就自动销毁就可以了
    //虽然auto_ptr 在c++ 11 新规范 被销毁了,但是我们还是要学习一下的
    template<class T>
    class auto_ptr{
    public:
    auto_ptr(T *p = 0): ptr(p){}
    ~auto_ptr(){ delete ptr;}
    private:
    T *prt;
    }
    void processAdoptions(istream& dataSource){
        while(dataSource){
            auto_ptr<ALA> pa(readALA(dataSource));
            pa->processAdoption();
            delete pa;
        }
    }
    

    auto_ptr的核心思想:以一个对象存放“必须自动释放的资源”,并依赖于改对象的destructor释放
    在这样的设计确实可以实现在pa->processAdoption()抛出异常的时候,保证内存不会泄露
    但是如果抛出异常是在constructor或者的、过程中呢?那么要如何处理呢?请看下文

    constructor 处理异常抛出导致的内存泄露(请配合more effective c++ 条款10阅读此点)

    代码参考more effective c++ 条款10(太长不想抄)

    1. 异常可能会因为operator new分配不到内存或者各种乱七八糟的错误,导致在constructor的时候出现问题
      抛出异常,那么会调用destructor吗?
      答案显然是不会,因为destructor只能析构已经构造完成的对象
      问题在于c++ 不允许对构造未完成的class调用destructor
    2. 针对于一些奇特的结构,theImage,theaudioclip为常量指针,那么就不能先初始化,在赋值了
      所以我们在初始化列表的时候使用对应的表达式,就没法使用try catch来捕获异常,(try catch属于语句)
      结合书上看,我们可以得知解决的方案使用一个内置的private函数来返回对应需要的指针,如果失败就抛出异常
      缺点也很明显:本来只需要一个structor实现的功能,却散布在很多函数中 当然使用auto_ptr 显然更为优秀
      看完这个条款,兜兜转转又回到auto_ptr

    destructor 处理异常抛出导致的内存泄露(请配合more effective c++ 条款11阅读此点)

    1. 存在一个有意思的东西,如果在调用destructor的时候抛出了对应的异常,恰巧还有个异常处于作用状态,然后你的程序就会
      被terminate函数直接干掉,(more effective c++描述了一个记日志的析构函数,如果直接干掉会导致信息并没有被写入日志)
    2. 如果让一个异常从destructor被抛出异常,那么严重的话会导致程序的结束,轻的话,会导致destructor函数执行不全

    exception 传递参数 调用虚函数 的区别(请配合more effective c++ 条款13阅读此点)

    class Widget{};
    void f1(Widget w);
    catch (Widget w);
    
    1. 函数与exception都有3种传递方式(1)by value(2)by pointer(3)by references
      调用函数的话,调用完函数,控制权会回到调用端;抛出exception,控制权不会再回到抛出端
      catch行为必定会保证copy,因为如果不copy直接返回对应对象的,一旦对象离开了他的生命范围
      那么肯定会调用destructor,那么catch子句就会收到一个仙逝的对象,emmmm显然这样就很不科学
      所以必须使用copy行为
    2. 当对象被复制当做一个exception时,复制的时候调用copy constructor,这个copy constructor相当于
      对象的静态类型而给动态类型,说了这么多,还是举个例子吧
    class Widget{...};
    class SpecialWidget: public Widget {...};
    void passAndThrowWidget(){
        SpecialWidget localSpecialWidget;
        ...//do somethings
        Widget & rw = localSpecialWidget;
        throw rw;//抛出了一个异常为Widget的exception
    }
    
    1. 使用throw 与 throw 变量是两种不同的意思,前者抛出当前的exception 后者抛出新的复制后的exception
    2. 最好不要使用by point抛出exception,如果抛出一个指向局部变量的指针,该局部变量会在exception离开scope
      时被销毁,当然抛出的指针显然是野指针
    3. catch捕捉异常基本不会涉及类型转换,除了如下的情况
      (1)继承架构中的类转换:一个针对base class exception的catch
      可以捕捉derived class的exception(通过more effective c++ 条款12 了解一下exception 的继承体系)
      (2)从一个“有型指针”转为“无形指针”,举个例子
    catch(const void *) ....//可以捕捉任何指针类型的exception
    
    1. catch 子句会依出现的顺序做匹配尝试
      与虚函数调用的机制进行对比,虚函数采用的是“best fit”机制,exception采用的是“first fit”
    2. throw 一个point
    void someFunction(){
        static exception ex;
        ......
        throw &ex;
    }
    void doSomething(){
        try{
            someFunction();//可能抛出一个 exception *
        }
        catch(exceptopm *ex){//捕捉到exception*
            ......           //没有任何对象被复制
        }
    }
    //4个标准的exceptions--bad_alloc,bad_cast,bad_typeid,bad_exception都是对象不是指针,所以无论如何只能采用
    //by value或者by references的方法来捕获异常
    
    1. catch丢出派生类,那么即使是派生类,但是调用的函数仍然是base class版,会存在切割的行为
    2. catch-by-references 可以解决上述第8点所出现的问题

    exception specification (more effective c++ 条款14)

    1. 什么是exception specification?
      有点难解释清楚,看个代码吧
    extern void f1();//可以抛出任何异常
    void f2() throw(int);//这个就是所谓的exception specification,只抛出类型为int的exceptions
    void f2() throw(int){
        f1();
    }
    //看个不太科学的template设计
    template<class T>
    bool operator==(const T& lhs, const T& rhs) throw(){
        return &lhs == &rhs;
    }
    //因为一旦抛出一个exception,那么exception specification被违反,那么将会导致unexpected。
    
    1. exception specification存在很多麻烦,因为编译器只对于它指向局部性检查,在templates使用它会有问题,很容易违背其准则,导致程序终止
    2. 处理一些非预期的 exception的方法
    //实现1
    class UnexpectedException{};
    void converUnexpected(){
        throw UnexpectedException();
    }
    //用converUnexpected函数取代默认的Unexcepted函数
    set_unexpected(converUnexpected);
    //实现2
    void converUnexpected(){//如果是非预期的exception被抛出,此函数被调用
        throw;              //只是重新抛出当前的exception
    }
    set_unexpected(convertUnexcept);//作为unexpected函数的替代品
    //exception specification都含有bad_exception,每个非预期的exception会被bad_exception取代
    //这个新的exception会代替非预期的exception传递下去                                                          
    
    建议还是看看书,没法说清楚这里 emmmm
    

    了解异常处理的成本(more effective c++ 条款15)

    1. exception是c++的一部分,编译器必须支持他们,就是这么回事
    2. 处理异常会比正常情况下慢3个数量级,所以能少用就少用

    吐槽一下throw

    这玩意在c++ 20被移除了,学个屁啊
    

    lazy evaluation(缓冲评估 more effective c++ 条款17)

    1. references counting(引用计数)
      举例:
    class String{...};
    string s1("hello world");
    string s2 = s1;//调用constructor copy
    //lazy 做法开始偷懒了,只是让s2共享s1,节省了调用new strcpy的高昂成本
    //知道涉及修改了,才提供一个副本
    
    1. lazy Fetching(缓式取出)
      假设涉及数据库的操作,那么一个类的体积如果非常大,如果要获取该对象的所有内容,那么可能成本非常的高
      在某些情况下,读取所有的数据是不必要的,所以这个方法的偷懒方式就是产生一个对象的外壳,不读取任何内
      容,当一个字段被需要了再去请求获取对应的字段
      修改const所对应的私有的值(可以参考const,因为我没看到写在哪里了,所以我决定再补一次)
      1. mutable可以解除const的效果
      2. 或者可以使用冒牌this指向 举个例子
    class LargeObject{
    public:
        const string & field1() const;
    private:
        string *fieldlValue;
    }
    const string & field1() const{
        LargeObject * const fakeThis = const_cast<LargeObject * const>(this);
        if(fieldlValur == 0){
            fkeThis->fieldlValue = ; 
        }
        return *fieldlValue;
    }
    
    1. lazy expression evaluation(表达式缓评估)
      总而言之一下,就是假设有运算c = a + b,如果你不使用那么他就不计算,使用一个结构来表示
      结构是两个指针 + 一个enum,知道要使用才计算
      注还是多看看书

    临时对象(more effective c++ 条款19)

    template<class T>
    void swap(T& object1, T& object2){
        T temp = object1;
        object1 = object2;
        object2 = temp;
    }
    
    1. c++ 真正的临时对象是不可见的,产生一个non-heap object 没有命名就产生了一个临时对象
      这种匿名对象出现的情况是
      (1)隐式类型转换
    size_t countChar(const string & str, char ch);
    char buffer[MAXN_STRING_LEN];
    char c;
    cin >> c >> setw(MAX_STRING_LEN) >> buffer;
    cout << countChar(buffer, c) << c << buffer << endl;
    
    这样会将char => const string&
    那么做法是产生一个string的临时对象。以buffer作为自变量,调用string constructor,将countChar的str参数绑定在string临时变量上
    当对象以by value传递,当对象传递给一个reference-to-const参数,转换才会发生
    reference-to-non-const,转换不会发生
    (2)当函数返回对象的时候
    

    上述概括并不详细,所以具体请看书
    这个地方应该补充一个c++ prime P107 现在暂时不补充

    智能指针,unique_ptr怎么实现的

    unique_ptr不能拷贝,如何实现

    1. 针对于unique_ptr函数不能拷贝 可以使用将其的拷贝构造函数,赋值运算符声明为private
    2. 或者令其拷贝构造函数,赋值运算符=delete
      auto_ptr在上文有描述,想了解的可以往上翻(利用destructors避免泄露资源 这一点)

    return value optimization(more effective c++ 条款20)

    C++允许函数在返回的时候将临时变量优化使得不存在。于是如果你这样调用operator,使用inline可以消除operator所花费的额外开销

    inline const Rational operator*(const Rational & lhs, const Rational & rhs){
        return Rational(lhs.numerator() * rhs.numerator(), lhs.numerator() * rhs.numerator());
    }
    Rational a = 10;
    Rational b(1, 2);
    Rational c = a * b;
    

    利用重载技术(overload)避免隐式类型转换(implicit type conversions) (参考more effective c++ 条款21)

    例子如下

    class UPInt {
    public:
        UPInt();
        UPInt(int value);
    };
    const UPInt operator+(const UPInt &lhs, const UPInt &rhs);
    UPInt upi1, upi2;
    UPInt upi3 = upi1 + upi2;
    upi3 = 10 + upi2;//这样是成立的,通过条款19,我们可以知道这个设计了隐式类型转换,临时变量
    const UPInt operator+(const UPInt &lhs, const UPInt &rhs);
    const UPInt operator+(int &lhs, const UPInt &rhs);
    const UPInt operator+(const UPInt &lhs, int &rhs);
    const UPInt operator+(int &lhs, int &rhs);  //错误
    //c++ 规定每个“重载操作符”必须获得至少一个“用户自定义”的自变量
    

    考虑以操作符复合形式(op=)取代其独身形式(op) (参考more effective c++ 条款22)

    x = x + y;
    x = x - y;
    //也可以写成
    x += y;
    x -= y;
    //注意如果x,y属于定制类型,不保证一定能够如此
    class Rational{
    public:
        Rational & operator+=(const Rational & rhs);
        Rational & operator-=(const Rational & rhs);
    };
    const Rational operator+(const Rational &lhs, const Rational &rhs){
        return Rational(lhs) += rhs;
    }
    const Rational operator-(const Rational &lhs, const Rational &rhs){
        return Rational(lhs) -= rhs;
    }//采用这种设计模式的话,只需要维护operator+=操作符,即操作符的复合形式
    template<class T>
    const T operator+(const T& lhs, const T& rhs){
        return T(lhs) += rhs;
    }
    template<class T>
    const T operator-(const T& lhs, const T& rhs){
        return T(lhs) -= rhs;
    }
    //一般来说,复合操作符比其独身形式的效率高,独身版本通常需要返回一个新对象
    //我们必须要承担一个constructor和一个deconstructor的代价,(前面的条款说会优化临时对象的影响
    //好像指的是没有命名的对象,采用inline的话,就没有临时对象产生的成本了),复合的好处
    //是直接将结果写入其左端自变量,所以不需要产生一个临时对象来放置返回值
    template<class T>
    const T operator+(const T& lhs, const T& rhs){
        return T(lhs) += rhs;//调用copy constructor
    }//写法1
    template<class T>
    const T operator+(const T& lhs, const T& rhs){
        T result(lhs);//调用copy constructor
        return result += rhs;//但是这种做法会导致value optimization无法施展
    }
    

    深拷贝和浅拷贝

    c++11

    右值引用

    lamda表达式

    malloc的实现方案

    malloc的实现方案:

    1)malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。
    2)调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。
    3)调用 free 函数时,它将用户释放的内存块连接到空闲链表上。
    4)到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

    operator->()重载

    1.如果point是指针,则按照内置的箭头运算符去处理。表达式等价于(*point).member。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类没有名为member的成员,则编译器报错。
    
    2.如果point是一个定义了operator->() 的类对象,则point->member等价于point.operator->() ->member。其中,如果operator->()的返回结果是一个指针,则转第1步;如果返回结果仍然是一个对象,且该对象本身也重载了operator->(),则重复调用第2步,否则编译器报错。最终,过程要么结束在第一步,要么无限递归,要么报错。
    

    大端小端

    大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
    小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。
    举个例子假设你存放的数据是0x12345678
    大端模式如下:
    0x12 0x34 0x56 0x78
    小端模式如下:
    0x78 0x56 0x34 0x12
    假设unsigned int value = 0x12345678 我们使用对应的unsigned char buf[4]来表示
    大端如下:
    buf[0] = 0x12
    buf[1] = 0x34
    buf[2] = 0x56
    buf[3] = 0x78
    小端如下:
    buf[0] = 0x78
    buf[1] = 0x56
    buf[2] = 0x34
    buf[3] = 0x12
    小端的好处
    计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
    常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式
    但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
    

  • 相关阅读:
    ASP的URL重写技术 IIS的ISAPI
    关于如何去勾引百度谷歌的蜘蛛爬虫
    如何让百度和google的蜘蛛爬虫迅速爬过来
    本机不安装Oracle客户端,使用PL/SQL Developer和 Instant Client 工具包连接oracle 11g远程数据库
    Java小知识点总结
    Tomcat启动分析(我们为什么要配置CATALINA_HOME环境变量)
    OracleDBconsoleorcl无法启动解决方案
    Java中PreparedStatement的错误使用
    Oracle 中行列转换问题总结
    Java中 Interger 的自动装箱
  • 原文地址:https://www.cnblogs.com/qq136155330/p/12423477.html
Copyright © 2020-2023  润新知