C++ Primer阅读心得(第十九章)

4 篇文章 0 订阅
  1. new表达式实际上做了三个操作:1.调用operator new分配一块原始内存;2.调用构造函数创建对象;3.返回该对象的指针。delete表达式实际上做了两个操作:1.调用析构函数销毁对象;2.调用operator delete释放内存。C++只允许我们重载operator new和operator delete,也就是定制内存的分配和释放流程,而其他的内置操作我们是修改不了的。
  2. 内存的控制流程(分配与回收)由三个组件组成:重载的operator new ([])、内存不够时的处理函数new_handler以及与重载的operator new对应的operator delete ([])。你既可以重载全局的operator new/delete,也可以针对某个对象重载operator new/delete。
    class MemControl {
    public:
        void* operator new(size_t size) { //重载标准库版本,默认为static
            auto origin_handler = std::set_new_handler(handler); //分配前装载自定义new_handler
            std::cout<<"Invoking customized operator new, size: " << size << std::endl;
            auto ret = ::operator new(size);
            std::set_new_handler(origin_handler); //正常处理后也别忘记恢复系统原始new_handler
            return ret;
        }
    
        void operator delete(void *pRawMem) { //重载标准库版本,默认为static
            std::cout << "Invoking customized operator delete "<< std::endl;
            ::operator delete(pRawMem);
        }
    
        static void handler() { //自定义版本new_handler,无内存分配时自动调用
            std::cout << "Memory allocation failed, terminating\n";
            std::set_new_handler(origin_handler); //抛出异常前恢复系统原始new_handler
            throw std::bad_alloc();
        }
    
    private:
        static std::new_handler origin_handler; //保存系统原始new_handler
        int big_array[100000000000000L]; //确保内存不够用,new_handler会被调用
    };
    std::new_handler MemControl::origin_handler = nullptr;
    
    MemControl *pmc = new MemControl(); //new表达式,调用重载了的operator new
                                        //如果内存不够用,则调用new_handler
    delete pmc;                         //delete表达式,调用重载了的operator delete
    
    
    注意:class中重载的operator new/delete默认为static,这是因为它们是在对象存在之前和存在之后才发挥作用,所以必须超脱对象的存在。
  3. 在系统的4个operator new/delete(noexcept*array[])之外,重载时加入了额外自定义参数的版本统一称为placement new/delete。其作用就是将new/delete表达式一分为二:1. 内存申请和回收完全交给你处理,需要你额外编写语句显式分配和回收;2.调用placement new之后,系统在你指定的地址调用构造函数。注意:有一个特殊版本operator new(size_t, void*)因为太常用被标准库收编了,你可以直接使用。该版本让系统在调用者传入的指定地址构造目标对象,这也就是placement new名称的由来。
    class Base {};
    
    auto pMem = ::operator new(sizeof(Base)); //手工分配空间
    Base *base = new(pMem) Base();            //调用标准库收编了的那个placement new,构造对象
    base->~Base();                            //手工析构
    ::operator delete(pMem);                  //手工释放
    注意:使用了placement new传入了你自己定制的参数时,operator delete的额外参数一定要和 operator new对应上。这是因为你为重载的operator new传入了一个额外的参数(例如:内存池Arena),内存分配成功但是构造函数抛出了异常,这时系统会直接调用operator delete来处理这个原始内存,如果你没有准备对应的附带额外参数的operator delete,那么系统将调用默认版本,这会导致内存的泄漏(没有调你的Arena把内存放回去)。
    class Arena { //最简单版内存池
    public:
        void* allocate(size_t size) {
            std::cout<<"Allocating with arena, size: " << size << std::endl;
            return ::operator new(size);
        }
    
        void deallocate(void *pBuffer) {
            std::cout<<"Deallocating with arena" << std::endl;
            return ::operator delete(pBuffer);
        }
    };
    
    class MemControl {
    public:
        MemControl () = default;
    
        MemControl (int i) { //构造函数抛异常,确保placement delete被调用
            throw std::runtime_error("Error when construct!");
        }
    
        void* operator new(size_t size, Arena& arena) { //传入内存池的placement new
            std::cout<<"Invoking placement new, size: " << size << std::endl;
            return arena.allocate(size);
        }
    
        void operator delete(void *pRawMem, Arena& arena) { //构造函数异常才会被调用
            std::cout << "Invoking placement delete when an exception occurs in constructor" << std::endl;
            arena.deallocate(pRawMem);
        }
    };
    
    Arena arena;
    try {
        MemControl *pmc_error = new(arena) MemControl(1); //触发构造函数异常
    } catch (std::runtime_error re) {
        std::cout << re.what() << std::endl;
    }
    
    MemControl *pmc = new(arena) MemControl(); //在内存池上构造对象
    pmc->~MemControl();                        //手工析构
    arena.deallocate(pmc);                     //手工释放
  4. 上面的内存池只是个为了说明做的例子,下面仿照protobuf的arena写个真正的简单版本内存池。

    typedef void(*destructor)(void*);
    class Arena {
    public:
        // destructor的转手函数
        template<class T>
        static void Destructor(void* ptr) {
            reinterpret_cast<T*>(ptr)->~T();
        }
    
        //为了方便,我们不希望用户先初始化一下Arena再使用,不然用户还要管理Arena生命周期
        //同时我们也不希望用户拿着singleton指针之后再使用,一个调用简单粗暴多好
        //此外,Arena应该为多个class共享更能节省内存,提升效率,所以应该全局化static
        //为了能够正确释放内存,必须搞定T对应的destructor
        //所以使用上面的Destructor函数模板转手调用析构函数
        //为了转发构造函数参数,接口必须是个变参模板
        template<class T, class... Args>
        static T* ConstructObject(Args&&... args) {
            if (pMem != nullptr) {
                pDestructor(pMem);
                ::operator delete(pMem);
            }
            pDestructor = Destructor<T>;
            pMem = ::operator new(sizeof(T));
            return new(pMem)T(std::forward<Args>(args)...);
        }
    private:
        // 内存地址和对应的destructor必须成对出现
        static void* pMem;
        static destructor pDestructor;
    };
    void* Arena::pMem = nullptr;
    destructor Arena::pDestructor = nullptr;
    
    //一个作为例子的class,任何的class都可以
    class MemControl {
    public:
        //下面这俩主要是为了输出创建和销毁的过程
        MemControl(int i) {
            std::cout << "MemControl " << i << " constructed" << std::endl;
            m_i = i;
        }
        ~MemControl() {
            std::cout << "MemControl " << m_i << " destructed" << std::endl;
        }
    
    private:
        int m_i = 0;
    };
    
    MemControl *pmc1 = Arena::ConstructObject<MemControl>(1); //创建一个
    MemControl *pmc2 = Arena::ConstructObject<MemControl>(2); //再创建一个的时候会销毁前面的
  5. C++的RTTI(RunTime Type Identification)运行时类型识别主要由两个运算符实现:

    1. dynamic_cast:负责在继承树上父类指针/引用到子类指针/引用的安全转换(反过来,子类转换到父类是默认转换,用不到这个)。安全指的是可以通过某种方式告知转换的失败:指针的转换如果失败,则返回空指针;引用如果转换失败,则抛出bad_cast异常。

      Base *pBase1 = new Base();
      Base *pBase2 = new Derived();
      Derived *pDerived1 = dynamic_cast<Derived*>(pBase1); //转换失败,返回nullptr
      Derived *pDerived2 = dynamic_cast<Derived*>(pBase2); //转换成功,转成子类指针
      std::cout << std::boolalpha << (pDerived1 == nullptr)
                << " " << (pDerived2 == nullptr) << std::endl; //true false
    2. typeid:传入一个表达式或者类型,返回一个type_info类型的常量引用来表示对应的类型,可以打印名称以及进行类型的比较。注意:typeid一个基类指针会返回基类指针类型,想获取该指针指向的真正类型需要给它加上一个*.

      //打印类型
      std::cout << typeid(pBase2).name() << std::endl;
      //无动态特性,返回基类指针类型
      std::cout << std::boolalpha
                << (typeid(pBase2) == typeid(Derived)) << std::endl; //false
      //加上*之后,有动态特性,返回子类类型
      std::cout << std::boolalpha
                << (typeid(*pBase2) == typeid(Derived)) << std::endl; //true
  6. C++11将C++98中那种enum定义为unscoped enum,新增了一种scoped enum,通过在enum关键字和名称之间加入一个class关键字实现,也被称为enum class。推荐尽可能的使用enum class,相比旧版本,它有以下优势:

    1. 没有名称污染问题。不加class的enum,会将enum成员的名称泄漏到定义它的代码域中造成名称污染,使得你无法定义重名的变量/类型。而enum class通过强化限定,将成员名称限制在enum内部,虽然你必须增加enum class名才能访问它们,但是不会造成名称污染的问题。

    2. 不会隐式转化为int(或更高类型)。不加class的enum的成员,会被隐式转化为int或者更高类型,因此存在在条件表达式或者函数调用时被误用(或者难以理解)的情况。而enum class的成员不能被隐式转化为int(当然可以被static_cast显式转化),所以可以避免以上的问题。

    3. 支持前置声明。C++98中不加class的enum,定义和声明必须放在一起,以方便编译器推断一个合适的成员类型,所以成员定义也会出现在.h文件中。如果后续增加或者减少成员,就会导致所有使用该enum定义的代码全部需要重新编译。C++11增加了enum前置声明支持将声明和定义分开以解决这个问题。注意:为了帮助编译器识别enum的成员的类型,非限制enum的前置声明必须指定成员类型,而enum class可以指定也可以不指定(默认为int)。

      enum UnScopedColor {red, yellow, blue};     //非限制enum,有名称污染
      enum class ScopedColor {red, yellow, blue}; //限制enum,也叫enum class
      
      int red = 1; //error,red已经被占用(污染)
      ScopedColor sc1 = green; //error,使用enum class的成员必须指定名称
      ScopedColor sc2 = ScopedColor::green; //ok,指定了enum class的名称
      
      UnScopedColor usc = red;
      if (usc < 4.5) { //可以隐式转化运行,但是代码可读性差,为啥要比较这俩?
          std::cout << "What does this mean?" << std::endl;
      }
      
      ScopedColor sc = ScopedColor::green;
      //if (sc < 4.5) { //error,不同类型无法比较
      if (sc < 4.5) { //ok,显式类型转换后可以比较
          std::cout << "OK, you forced it!" << std::endl; //嗯,你是故意的
      }
      
      enum UnScopedColor2 : int;  //非限制enum的前置声明,必须定义成员类型
      enum class ScopedColor2;    //限制enum的前置声明,默认为int
      
      enum UnScopedColor2 : int {red, yellow, blue}; //前置声明为int,这里也必须为int
      enum class ScopedColor2 {red, yellow, blue, green};
  7. 类成员指针是指向类的非静态成员的指针,采用class_name::* var_name的方式声明(注意其中的::*),既可以指向数据成员也可以指向方法成员。类成员指针可以想象为指向一个类内部的“偏移量”的指针,定义后无法直接使用,必须与一个该类的真实实例结合才能使用,相当于在真实地址上附加了这个“偏移量”就指向了有效的地址。注意:类成员函数指针无法直接调用,因此无法被用在STL算法中,需要使用标准库函数mem_fn包装一下才行。

    class PtrAccess {
    public: //注意访问权限,private的话外界无法访问的
        std::string name{"default"};
        void say_hello() {
            std::cout << "Hi there! My name is " << name << std::endl;
        }
    };
    
    //定义类成员指针,注意声明方式(尤其是函数指针),推荐使用auto偷懒
    std::string PtrAccess::* ptr_data = &PtrAccess::name;
    void (PtrAccess::* ptr_function)() = &PtrAccess::say_hello;
    
    //访问例子1:对象变量使用.*访问
    PtrAccess pa;
    pa.*ptr_data = "Access by ::* and .*";
    (pa.*ptr_function)(); //注意:前面那个括号是必须的,因为函数调用运算符的优先级高于.*
    
    //访问例子2:指针变量使用->*访问
    PtrAccess* ppa = new PtrAccess();
    ppa->*ptr_data = "Access by ::* and ->*";
    (ppa->*ptr_function)();
    delete ppa;
    
    //以上两种访问方式可以这样理解:
    //1.先使用*操作符作用于类成员指针,解地址后获得真正的指向(偏移量+真实地址?)
    //2.使用.或者->访问成员
    
    //使用mem_fn包装后放入STL的算法中使用
    std::find_if(svec.begin(), svec.end(), std::mem_fn(&std::string::empty));
  8. 定义在另外一个类内部的类被称为嵌套类(nested class),定义在一个函数内部的类被称为局部类(local class),它们都能够帮助进行代码的封装。

    • 嵌套类必须声明在类的内部,但是其定义可以放在类的外部,定义和外界访问时必须标明外层class的名称加上嵌套类的名称才能使用。嵌套类受到public/protected/private的访问限制,非public的外界无法使用该类型。

      class Person {
      private:
          //嵌套类,private确保了代码的隔离性,这个Address我就不想别人用
          class Address {
          public:
              std::string city;
              std::string street;
              void show_address();
          };
      public:
          std::string name;
          Address address;
          void show();
      };
      
      //嵌套类成员函数可以定义在外,但是增加外部class的名称
      void Person::Address::show_address() {
          std::cout << city << " " << street << std::endl;
      }
      
      void Person::show() {
          std::cout << name;
          address.show_address();
      }
      
      Person p;
      p.name = "Me";
      p.address.city = "Beijing";
      p.address.street = "Haidian";
      p.show();
    • 局部类必须全部定义在函数内部(外面也没地方放啊),因此通常比较简单(类似struct)。局部类内部还可以嵌套一个类...(丧心病狂啊)

      void parse_config(){
          // 定义一个local class
          // 与tuple相比,变量有名,更易读
          // 与外部class相比,封装更紧密,不让外界用
          class Config {
          public:
              int interval;
              int level;
              int speed;
              void show() {
                  std::cout << interval << " "
                            << level << " "
                            << speed << std::endl;
              }
          };
      
          // 使用local class
          Config c;
          c.interval = 1;
          c.level = 2;
          c.speed = 3;
          c.show();
      }
      
      //调用该函数,输出:1 2 3
      parse_config();
  9. Union与struct类似,有构造函数和析构函数,有public/protected/private的访问控制,默认访问类型为public,可以使用{}初始化。它们之间的主要区别在于union中只有一个成员会生效(因此更省空间),union无法继承和派生(因此没有virtual函数),union内部也不允许有引用成员。

    union Token {
        //默认为public
        Token();            // 构造函数
        ~Token() = default; // 析构函数
        void say_hello();   // 其他函数
    
        //以下只有一个会生效
        int int_token;
        double double_token;
        char char_token[10];
    };
    
    Token::Token() {
        int_token = 0;
    }
    
    void Token::say_hello() {
        std::cout << "Hi there! I'm an union." << std::endl;
    }
    
    Token t;
    std::cout << t.int_token << std::endl; //构造函数默认为int_token
    t.say_hello();
    t.double_token = 1.0; //替换为double_token, 系统不保证使用另外两个不出错
    std::cout << std::showpoint <<t.double_token << std::endl;

    如果union中包含了一个类对象(C++11),则你必须负责手工调用这个类的构造函数和析构函数(因为union不知道运行时的具体类型因此无法自动调用)。匿名的union与非限制enum类似,成员变量都是泄漏到定义域中的(可以访问)。Union的坑在于它内部究竟是哪个成员生效你需要额外记录它,一旦用错程序就挂掉了。所以通常的办法是:给它配一个enum做判别式,同步标识它内部是什么类型;然后再用一个管理class同时管理union和enum,因为union和enum都定义在管理class内部所以可以不给它们起名(匿名union和非限定enum)。

    class Token2 { //管理类
    public:
        // 各种构造函数对应不同的value类型
        Token2() : data_type(INT), int_token(0) {}
        Token2(int ival) : data_type(INT), int_token(ival) {}
        Token2(double dval) : data_type(DBL), double_token(dval) {}
        Token2(const std::string& str) : data_type(STR), str_token(str) {}
    
        // 赋值操作符
        Token2 &operator= (const Token2& t) {
            using namespace std;
            if (data_type == STR && t.data_type != STR) {
                //如果内容不再是string,必须手工销毁
                str_token.~string();
            }
    
            switch(t.data_type) {
                case INT:
                    int_token = t.int_token;
                    break;
                case DBL:
                    double_token = t.double_token;
                    break;
                case STR:
                    if (data_type != STR) {
                        //本来不是string,变成string需要用placement new手工初始化
                        new(&str_token) string(t.str_token);
                        //str_token = t.str_token; //直接赋值是错误的,必须手工
                    } else {
                        str_token = t.str_token; //本来是string那就复用
                    }
                    break;
            }
            data_type = t.data_type;
            return *this;
        }
    
        ~Token2() {
            if (data_type == STR) {
                using namespace std;
                str_token.~string(); //必须手工销毁
            }
        }
    
    private:
        //非限定enum,指示了union中的数据类型
        enum {INT, DBL, STR} data_type;
    
        union { //匿名union,成员直接泄漏到管理class中
            int int_token;
            double double_token;
            std::string str_token;
        };
    };
    
    Token2 t2("abcdefghijklnm");
    Token2 t3(123);
    t3 = t2;
  10. 其他不可移植特性

    • 位域:可以为class/struct的非静态数据成员指定它占用几个bit,这在数据内存对齐时非常有用。

    • volatile限定符:告诉编译器这个变量可能再程序控制、检测之外被改变,不要在编译中优化它(不要妄想这个特征与java一样对多线程有效)。volatile与const很像,有volatile变量、volatile指针、指向volatile变量的指针以及指向volatile变量的volatile指针。注意:系统合成的拷贝控制三大件(拷贝、移动和赋值)对volatile对象无效,因为它们的参数是const &,如果你需要可以自己定义。

    • 链接指示:extern “C”,表明这个函数是用其他语言写的,需要编译器特殊对待。链接指示支持单行和大括号包裹的多行两种模式,如果头文件被包含了进去,那么该头文件中所有普通函数都被extern了。C函数指针类型定义时必须在前面加上extern “C”,而且它和C++函数指针类型是两种不同的类型,即使参数和返回值都一致,两者也不能互相赋值。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值