new 和 delete
class A
{
public:
A()
{
}
~A()
{
}
};
A *s =new A();
new 的具体操作过程分三步
1. 调用标准库函数operator new申请足够大的,原始的,未命名的内存空间
2. 编译器使用A的构造函数构造对象,并为其传入初始值
3. 返回构造完成后的对象的指针
delete 的具体操作分两步
1. 执行A的析构函数
2. 调用标准库函数operator delete释放空间
标准库中定义了operator new 和operator delete 函数总共8个版本
其中std::nothrow_t 是定义在new中的一个空struct. new中还定义了一个名为std::nothrow的const对象。用户可以用它请求new的不抛出异常的版本。
void * operator new(size_t);
void * operator new[](size_t);
void operator delete(void *)noexcept;
void operator delete[](void *)noexcept;
void * operator new(size_t, std::nothrow_t&)noexcept;
void * operator new[](size_t, std::nothrow_t&)noexcept;
void operator delete(void *,std::nothrow_t&)noexcept;
void operator delete[](void *, std::nothrow_t&)noexcept;
用户可以自定义上面版本的任意一个。前提是自定义版本必须位于全局作用域或者类作用域。当将上述运算符定义成类的成员时,它们是隐式静态的。
class A
{
public:
void * operator new(size_t s)
{
std::cout << "2222222 " << s << std::endl;
void * t;
return t;//这是个bug,因为我们没有分配内存
}
A()
{
std::cout << "111111" << std::endl;
s = new char(5);//这里会报segment fault.因为我这里会在operator new 返回的内存空间里中构造对象。但是我们的operator new返回一个空的void*
}
char *s;
};
int main()
{
A *s = new A();//这里先调用operator new, operator new 会优先使用A类下面的operator new 成员函数,
}
operator new
用户可以自定义自己的operator new 运算符。
new本身可以被传递额外的参数
我们可以定义自己的new版本,让其包含其他类型的参数,如int(尽管这个int 没啥用)
class A
{
public:
void * operator new(size_t s, int m)//operator new运算符号需要有额外的参数int
{
std::cout << "2222222 " << s << std::endl;
void * t;
return t;
}
A()
{
std::cout << "111111" << std::endl;
s = new char(5);
}
char *s;
};
int main()
{
A *s = new(1) A();// 这里我们在new后面增加了一个(1),它意味着我们向operator new运算符号需要有额外的参数int
}
class A
{
public:
void * operator new(size_t s, int m,int n)//operator new运算符号需要两个额外的参数int
{
std::malloc
return t;
}
A()
{
std::cout << "111111" << std::endl;
s = new char(5);
}
char *s;
};
int main()
{
A *s = new(1,2) A();// 这里我们在new后面增加了一个(1,2),它意味着我们向operator new运算符号需要有两个额外的参数int
}
两点注意
一、关于operator new
C++ 规定不准用户重载
void *operator new(size_t, void*);
这个只能标准库自己使用。
二、关于operator delete
operator delete或者operator delete[]定义类的成员时候,可以包含另外一个类型为size_t 的参数,此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有个虚析构函数,则传递给operator delete的字节数将因待删除指针所指向的动态类型不同而有所区别。实际运行的operator delete函数版本也由对象类型决定。
malloc 和 free
当定义自己的operator new的时候,必须调用这两个函数来执行内存申请和释放的操作
class A
{
public:
void * operator new(size_t s, int m,int n)
{
if(void* mem = malloc(s))
{
return mem;
}
else
{
throw std::bad_alloc();
}
}
void operator delete(void *s)noexcept
{
free(s);
}
A()
{
}
~A()
{
}
};
int main()
{
A *s = new(1,2) A();
}
Placement New(定位new)
placement new 是 operator new 的重载版本。
我们 可以在调用new 的时候,可以给 new 传递一个额外的参数,如上个例子中的int等。但是operator new 不允许我们重载new的带有指针类型的参数。
但是我们可以以直接调用
type A = new(ptr) A()
当我们这样的形式调用new的时候,编译器就会按照特定的规则执行new 操作。也就是placement new.
placement new 的形式如下:
当通过一个地址值调用new时,placement new依靠下面这个特定的new版本”分配内存”
void *operator new(size_t, void* p)
{
return p;//事实上它只是简单的返回给定的指针
}
class A
{
public:
A()
{
}
~A()
{
}
std::string s;
};
int main()
{
void *s;
A *a = new (s)A();//程序将在s指针指向的地址上构造A的对象
//不过这个程序有bug,因为首先s 是个空指针,然后试图在这个空指针指向的内存中创建对象。
}
标准库有个allocator 类,它的成员函数allocate/deallocate和operator new/delete功能类似,都是分配空间,但是不构造对象. allocator类通过调用construct构造对象,这一点和 placement new相似,两者都是构造对象的,但是转给construct 的指针必须指向同一个allocator 分配的空间。传给placement new 的指针则没有限制,只要是个地址就可以了。
我们可以利用placement new 实现对一段地址的反复利用
class A
{
public:
A()
{
std::cout << "1111" << std::endl;
}
void test()
{
std::cout << "@@@@@@@ " << a << std::endl;
}
void set()
{
a = 10;
}
~A()
{
std::cout << "22222" << std::endl;
}
private:
int a;
};
int main()
{
char a[sizeof(A)];//方式1:栈上分配
//char *a = new char(sizeof A);//方式2: 堆上分配
//int *ptr= new int(4);
//void *a = reinterpret_cast<void *>(ptr);//方式3:重新解释
A * m = new (a) A();
m->set();
m->test();
m->~A();//这里只会调用析构函数体,却不会对内存进行释放,因为释放是 调用operator delete操作符来实现的
A *n = new (m) A();
n->test();
}
类型转换
static_cast的用法
1.基本类型的转换
double f =3.0;
int a = static_cast<int>(f);
2.任意类型的表达式转成void*类型
3. void 指针转换为目标类型
void * s = static_cast<void*>(new B());
B * m = static_cast<B*>(s);
4.进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的
class A
{
};
class B : public A
{
public:
int *m;
};
int main()
{
A* s = new A();
B* m = static_cast<B*>(s);//下行转换,这里事实上是错误的。但是编译器不会报错。运行时也不会报错,但是如果用了这个指针去访问B的成员,会报错,也就是不是安全的
std::cout << *m->m << std::endl;//这里访问了一个不存在的变量,程序崩溃
}
dynamic_cast的用法
dynamic 运算符的使用形式如下:
dynamic_cast < type-id > ( expression )
type-id 必须是个类的指针或者引用,或者void*
dynamic 可以用于类的上行或者下行转换。
上行转换
派生类向基类转换
class A
{
};
class B : public A
{
public:
int *m;
};
int main()
{
B* s = new B();
A*n = dynamic_cast<A*>(s);//这里用static_cast替换掉dynamic_cast,也是同样的
}
下行转换
基类向派生类转换。
这里要求基类类型含有虚函数。
class A
{
public:
virtual void test()//必须至少有一个成员函数是虚函数
{}
};
class B : public A
{
public:
int *m;
};
int main()
{
A* s = new A();
B* m = dynamic_cast<B*>(s);//这里转换时会进行运行时的检测,如果转换失败,会返回空指针
if(m)
std::cout << *m->m << std::endl;//成功转换的时候,我们才能安全的利用该指针访问其成员函数
else
std::cout << "1111111" << std::endl;
void *kk = dynamic_cast<void*>(s);//转换为void *的用法和 static_cast类似
}
更安全的写法
A* s = new A();
if(B* m = dynamic_cast<B*>(s))//这样写的好处保证我们在if语句外不会访问到m变量
std::cout << *m->m << std::endl;
else
std::cout << "1111111" << std::endl;
dynamic_cast 可以转换引用,但是不存在所谓的空引用 ,因此引用类型无法使用与指针类型完全相同的错误报告策略。判断引用转换成功的方式是捕获std::bad_cast异常
A* s = new A();
try
{
const B& t = dynamic_cast<const B&>(*s);
}
catch (std::bad_cast e)
{
std::cout << e.what() << std::endl;
}
typeid
使用方式 typeid(e), e 可以使表达式或者类型的名字
返回的结果是常量对象的引用,该对象标准库类型是type_info或者type_info 的共有派生类型
当e不属于类类型或者是个 不包含任何虚函数的类时,typeid指示的是运算对象的静态类型
A* s = new A();
B *s2 = new B();
std::cout << (typeid(s) == typeid(A)) << std::endl;//False, s是个指针,因此typeid的结果是个s的静态类型,s的静态类型是A*
std::cout << (typeid(s) == typeid(A*)) << std::endl;//True
typeid 对一个空的指针进行解引用求值时,会抛出std::bad_typeid的错误
A* ret()
{
return nullptr;
}
int main()
{
try
{
typeid(*ret());
}
catch (std::bad_typeid e)
{
std::cout << e.what() << std::endl;
}
}
应用
判断两个对象是否相等的方法是对象的类型和成员取值是否一致
当派生类对象需要判断相等的方法时,普通做法是在基类定义一个虚函数,同时让派生类各自实现各自的相等的方法。
这个方法通常难以奏效,因为虚函数要求派生类和基类的形参是一致的
class A
{
public:
virtual void equal(A&){
}
};
class B:public A
{
public:
void equal(A&)override{
//B的test方法必须和A的完全一致,才能实现覆盖
}
}
这样继承类的equal方法只能比较基类成员,而不能比较派生类独有的成员。
这时可以采用 运行时类型识别的方法来解决
class A
{
public:
friend bool operator==(const A&, const A&);
protected:
virtual bool equal(const A&) const
{
}
};
class B : public A
{
protected:
virtual bool equal(const A&s) const override
{
auto r = dynamic_cast<const B&>(s);//我们确定了两个类型是一致的,所以不会抛出异常
//继续比较派生类成员
}
};
bool operator==(const A&s1 , const A&s2)
{
return typeid(s1) == typeid(s2) && s1.equal(s2);//s1和s2类型一致,则进入虚函数equal,不一致则直接返回false
}
枚举
C++11引入了限定作用域的枚举类型
enum A{ m,n,o};//不限定作用域的枚举
enum struct B{m,n,o};//限定作用域的枚举
enum class C{m,n,o}//限定作用域的枚举
A a = m;//正确
B m2 = m;//错误,不限定作用域的枚举值不能初始化限定作用域的枚举类型
B m3 = B::m;//正确
C m2 = B::m;//错误,二者类型不一致
std::cout << m << std::endl;//可以直接输出,输出0
std::cout << B::m << std::endl;//编译错误
std::cout << int(B::m) << std::endl;//正确,限定作用域枚举类型必须显示转化,输出0
默认情况下,限定作用域的enum成员类型是int,可以将其指定为其它整型类型
enum A: unsigned long long
{
m = 18446744073709551615ULL
}
类成员指针
成员指针是类的非静态成员的指针。
数据成员指针
class A
{
public:
A() :data("sds") {}
const std::string data;
static const std::string A::* data()//返回成员指针
{
return &Screen::data;//成员指针指向A类的成员,而非实际数据。使用成员指针,必须把它绑定到A类类型的对象上
}
};
int main()
{
const std::string A::*pdata;//指向A类的const string 成员的指针
pdata = &A::data;//该指针没有指向任何数据
std::cout << pdata << std::endl;//!!!输出1
A s1, *s2 = &s1;
std::cout << s2->*pdata << std::endl;//pdata的使用方式,首先解引用成员指针,获得所需的成员,再用成员访问运算符获取成员
}
成员函数指针
class A
{
public:
A() :data("sds") {}
const std::string data;
void test(int tt)
{
std::cout << data << std::endl;
}
};
int main()
{
A s1, *s2 = &s1;
void (A::*func)(int);//声明成员函数指针
func = &A::test;
std::cout << func << std::endl; //输出1
(s1.*func)(0);//类似数据成员指针,必须绑定对象使用
using MM = void (A::*)(int);//可以使用别名
MM func2 = &A::test;
(s1.*func2)(1);
}
将成员函数用作可调用对象
int main()
{
std::vector<std::string> s;
auto fp = &std::string::empty;//使用string的成员函数empty的指针
std::find_if(s.begin(), s.end(), fp);//错误,我们没有传入对象,成员指针只能使用.* 或者->*来调用
}
使用function 生成一个可调用对象
#include<functional>
int main()
{
std::vector<std::string> s;
std::function<bool(const std::string&)> fcn = &std::string::empty;
std::find_if(s.begin(), s.end(), fcn);
}
使用mem_fn生成一个可调用对象
find_if(s.begin(),s.end(), mem_fn(&string::empty));
//std::mem_fn在头文件functional中,std::mem_fn 可以根据成员函数指针的类型推断可调用对象的类型,而无需用户显示指定
使用bind生成一个可调用对象
find_if(s.begin(), s.end(), std::bind(&std::string::empty, std::placeholders::_1));
union
union 不能包含引用类型的成员。一个union任何时候只有一个数据成员有值,分配给union的存储空间至少容纳它的最大数据成员。默认情况下,union是public的。也可以指定某些成员为private, protected。
匿名union
union{
char a;
int m;
};
a = 'c';//为union对象赋一个新值
m = 10;//该对象当前为10
C++11 允许union包含类类型的成员
如果类类型成员定义了自己的构造函数,析构函数,则union必须也定义构造函数和析构函数。如果类类型成员没有 定义的话,则union也不需要定义
class A
{
public:
A()
{
std::cout << "A construct" << std::endl;
}
~A()
{
std::cout << "A destruct" << std::endl;
}
};
union K{
K()
{}//A定义了构造函数,所以K 必须定义
~K()
{}//A定义了析构函数,所以K 必须定义
char a;
int m;
A h;
};
int main()
{
K q;
q.a = 'c';//为union对象赋一个新值
q.m = 10;//该对象当前为10
q.h = A();
}
通常可以将union以匿名的方式放在类里面作为类成员,由类负责构造或者析构
class A
{
public:
A() :tag(INT),a(0)
{
}
~A()
{
if (tag == STR)
{
c.std::string::~string();//需要 显示释放std::string 成员,union只会析构基本类型,std::string内置的char指针需要单独释放
}
}
A&operator=(A& s)
{
if (tag == STR && s.tag != STR) c.std::string::~string();
if (tag == STR && s.tag == STR)
c = s.c;
else
copyToUnion(s);
tag = s.tag;
return *this;
}
A &operator=(const std::string &g)
{
if (tag == STR)
{
c = g;
}
else
new(&c) std::string(g);//!!!原位赋值
tag = STR;
return *this;
}
A &operator=(const int &g)
{
if (tag == STR)
{
c.std::string::~string();
}
tag = INT;
a = g;
return *this;
}
A &operator=(const char &g)
{
if (tag == STR)
{
c.std::string::~string();
}
tag = CHAR;
b = g;
return *this;
}
void test()
{
std::cout << &a << std::endl;//返回首地址
std::cout << &b << std::endl;//返回首地址
std::cout << &c << std::endl;
}
private:
enum { INT, CHAR, STR } tag;//用来指明union中存在的成员类型
union
{
int a;
char b;
std::string c;
};
void copyToUnion(const A&s)
{
switch (s.tag)
{
case INT: a = s.a; break;
case CHAR:b = s.b; break;
case STR:
new(&c) std::string(s.c); break;
default:
break;
}
}
};
局部类
定义在函数的内部的类,被称为局部类
局部类不允许声明静态数据成员
局部类只能访问外层作用域定义的类型名,静态变量和枚举成员
int val = 10;
void foo(int val)
{
static int ai = 5;
enum Loc{ a =1024, b};
struct Bar
{
Loc l;// 使用一个局部类型名
int barvar;
void fooBar(Loc l = a)
{
barvar = val;//错误,不能访问函数作用域的普通变量
barvar = b;//使用枚举成员
barvar = ::val;//使用全局变量
l = ai;//使用静态局部对象
}
};
}
位域
取地址运算符无法作用于位域
typedef unsigned int Bit;
class K
{
Bit mod : 2;//mod占2个二进制位
Bit modified : 1;
Bit prot_owner : 3;
public:
enum modes
{
READ = 01,
WRITE = 02,
EXE = 03
};
K &write()
{
modified = 1;
}
A& open(modes m)
{
mod != READ;
if (m & WRITE)
{
//...
}
}
inline bool isRead()const { return mod &READ; }
void test()
{
auto n = &mod;//错误
}
};
volatile限定符
当对象的值可能在程序的控制或者检测之外被改变时,volatile告诉编译器不应该对这样的对象进行优化。例如程序可能包含一个由系统时钟定时更新的变量。
不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile 对象或者从volatile赋值
链接指示extern “C”
链接指示不能出现在类或者函数定义的内部。链接指示必须在函数的每个声明都出现
除了extern “C”外,还可以有其他语言指示,如 extern “FORTRAN”, extern “Ada” 等
extern "C"
{
#include<string.h>//将string.h的所有函数链接起来。头文件的所有普通函数声明都被认为是由链接指示的语言编写的
}
链接指示还可以让C++函数在其他语言编写的程序中使用
extern "C" double calc(double param);
有时候需要C 和C++ 编译同一个源文件,在编译C++版本的程序时,可以使用预处理器定义__cplusplus
#ifdef __cplusplus
extern "C"
#endif
int strcmp(const char*,const char*);