- new表达式实际上做了三个操作:1.调用operator new分配一块原始内存;2.调用构造函数创建对象;3.返回该对象的指针。delete表达式实际上做了两个操作:1.调用析构函数销毁对象;2.调用operator delete释放内存。C++只允许我们重载operator new和operator delete,也就是定制内存的分配和释放流程,而其他的内置操作我们是修改不了的。
- 内存的控制流程(分配与回收)由三个组件组成:重载的operator new ([])、内存不够时的处理函数new_handler以及与重载的operator new对应的operator delete ([])。你既可以重载全局的operator new/delete,也可以针对某个对象重载operator new/delete。
注意:class中重载的operator new/delete默认为static,这是因为它们是在对象存在之前和存在之后才发挥作用,所以必须超脱对象的存在。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
- 在系统的4个operator new/delete(noexcept*array[])之外,重载时加入了额外自定义参数的版本统一称为placement new/delete。其作用就是将new/delete表达式一分为二:1. 内存申请和回收完全交给你处理,需要你额外编写语句显式分配和回收;2.调用placement new之后,系统在你指定的地址调用构造函数。注意:有一个特殊版本operator new(size_t, void*)因为太常用被标准库收编了,你可以直接使用。该版本让系统在调用者传入的指定地址构造目标对象,这也就是placement new名称的由来。
注意:使用了placement new传入了你自己定制的参数时,operator delete的额外参数一定要和 operator new对应上。这是因为你为重载的operator new传入了一个额外的参数(例如:内存池Arena),内存分配成功但是构造函数抛出了异常,这时系统会直接调用operator delete来处理这个原始内存,如果你没有准备对应的附带额外参数的operator delete,那么系统将调用默认版本,这会导致内存的泄漏(没有调你的Arena把内存放回去)。class Base {}; auto pMem = ::operator new(sizeof(Base)); //手工分配空间 Base *base = new(pMem) Base(); //调用标准库收编了的那个placement new,构造对象 base->~Base(); //手工析构 ::operator delete(pMem); //手工释放
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); //手工释放
-
上面的内存池只是个为了说明做的例子,下面仿照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); //再创建一个的时候会销毁前面的
-
C++的RTTI(RunTime Type Identification)运行时类型识别主要由两个运算符实现:
-
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
-
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
-
-
C++11将C++98中那种enum定义为unscoped enum,新增了一种scoped enum,通过在enum关键字和名称之间加入一个class关键字实现,也被称为enum class。推荐尽可能的使用enum class,相比旧版本,它有以下优势:
-
没有名称污染问题。不加class的enum,会将enum成员的名称泄漏到定义它的代码域中造成名称污染,使得你无法定义重名的变量/类型。而enum class通过强化限定,将成员名称限制在enum内部,虽然你必须增加enum class名才能访问它们,但是不会造成名称污染的问题。
-
不会隐式转化为int(或更高类型)。不加class的enum的成员,会被隐式转化为int或者更高类型,因此存在在条件表达式或者函数调用时被误用(或者难以理解)的情况。而enum class的成员不能被隐式转化为int(当然可以被static_cast显式转化),所以可以避免以上的问题。
-
支持前置声明。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};
-
-
类成员指针是指向类的非静态成员的指针,采用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));
-
定义在另外一个类内部的类被称为嵌套类(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();
-
-
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;
-
其他不可移植特性
-
位域:可以为class/struct的非静态数据成员指定它占用几个bit,这在数据内存对齐时非常有用。
-
volatile限定符:告诉编译器这个变量可能再程序控制、检测之外被改变,不要在编译中优化它(不要妄想这个特征与java一样对多线程有效)。volatile与const很像,有volatile变量、volatile指针、指向volatile变量的指针以及指向volatile变量的volatile指针。注意:系统合成的拷贝控制三大件(拷贝、移动和赋值)对volatile对象无效,因为它们的参数是const &,如果你需要可以自己定义。
-
链接指示:extern “C”,表明这个函数是用其他语言写的,需要编译器特殊对待。链接指示支持单行和大括号包裹的多行两种模式,如果头文件被包含了进去,那么该头文件中所有普通函数都被extern了。C函数指针类型定义时必须在前面加上extern “C”,而且它和C++函数指针类型是两种不同的类型,即使参数和返回值都一致,两者也不能互相赋值。
-