- 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 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, terminatingn";
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,这是因为它们是在对象存在之前和存在之后才发挥作用,所以必须超脱对象的存在。在系统的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); //手工释放 上面的内存池只是个为了说明做的例子,下面仿照protobuf的arena写个真正的简单版本内存池。
typedef void(*destructor)(void*);
class Arena {
public:
// destructor的转手函数
template
static void Destructor(void* ptr) {
reinterpret_cast(ptr)->~T();
}
//为了方便,我们不希望用户先初始化一下Arena再使用,不然用户还要管理Arena生命周期
//同时我们也不希望用户拿着singleton指针之后再使用,一个调用简单粗暴多好
//此外,Arena应该为多个class共享更能节省内存,提升效率,所以应该全局化static
//为了能够正确释放内存,必须搞定T对应的destructor
//所以使用上面的Destructor函数模板转手调用析构函数
//为了转发构造函数参数,接口必须是个变参模板
template
static T* ConstructObject(Args&&... args) {
if (pMem != nullptr) {
pDestructor(pMem);
::operator delete(pMem);
}
pDestructor = Destructor;
pMem = ::operator new(sizeof(T));
return new(pMem)T(std::forward(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(1); //创建一个
MemControl *pmc2 = Arena::ConstructObject(2); //再创建一个的时候会销毁前面的 C++的RTTI(RunTime Type Identification)运行时类型识别主要由两个运算符实现:
dynamic_cast:负责在继承树上父类指针/引用到子类指针/引用的安全转换(反过来,子类转换到父类是默认转换,用不到这个)。安全指的是可以通过某种方式告知转换的失败:指针的转换如果失败,则返回空指针;引用如果转换失败,则抛出bad_cast异常。
base *pbase1 = new base(); base *pbase2 = new Derived(); Derived *pDerived1 = dynamic_cast(pbase1); //转换失败,返回nullptr Derived *pDerived2 = dynamic_cast (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 < 如果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++函数指针类型是两种不同的类型,即使参数和返回值都一致,两者也不能互相赋值。



