C++Primer 第十九章

时间:2023-03-08 21:58:25
//1.控制内存分配:
//A:某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,比如使用关键字new将对象放置在特定的内存空间中。
//B:如下代码:
string *sp = new string("s");
string *arr = new string[];
// new系列运算符实际上执行了三步操作:
// 第一步:new表达式调用一个名为operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未经构造的内存空间。
// 第二步:编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
// 第三步:对象被分配了空间并完成构造,返回一个指向该对象的指针。
delete sp;
delete []arr;
// delete系列运算符实际上执行了两步操作:
// 第一步:对sp所指对象或者arr所指的数组中的元素执行对应的析构函数。
// 第二步:编译器调用名为operator delete(或者operator delete[])的标准库函数释放空间。
// 如果程序希望控制内存分配的过程,则需要自己定义operator new函数和operator delete函数。这两个函数的查找过程为:首先在对应类的类空间中查找(若操作的对象是类类型的话),其次在全局空间中查找,最后会调用标准库的版本。
//C:标准库定义的operator new系列函数和operator delete系列函数,其中带nothrow_t的是不抛出异常的版本:
_Ret_bytecap_(_Size) void *__CRTDECL operator new(size_t _Size) _THROW1(_STD bad_alloc);
_Ret_bytecap_(_Size) void *__CRTDECL operator new[](size_t _Size);
_Ret_opt_bytecap_(_Size) void *__CRTDECL operator new(size_t _Size, const _STD nothrow_t&);
_Ret_opt_bytecap_(_Size) void *__CRTDECL operator new[](size_t _Size, const _STD nothrow_t&); void __CRTDECL operator delete(void *) _THROW0();
void __CRTDECL operator delete[](void *) _THROW0();
void __CRTDECL operator delete(void *, const _STD nothrow_t&);
void __CRTDECL operator delete[](void *, const _STD nothrow_t&); struct nothrow_t
{ // placement new tag type to suppress exceptions
};
extern const nothrow_t nothrow; // constant for placement new tag
// 应用程序可以自定义上述的任何版本,前提是自定义的版本必须位于全局作用域或者是类作用域。当将上述函数定义为类的成员函数的时候,其是隐式静态的,因为operator new系列函数用于对象构造之前,而operator delete系列函数用于对象销毁之后,所以此类函数不能操作类的数据成员。
// 对于operator new系列的函数,它们的返回值必须是void*,第一个形参类型必须是size_t且不能有默认实参,此参数用于保存构造内存所需的字节数。
// 如果想要自定义operator new系列函数,则可以为其提供额外的参数。此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传递给新增的形参。
void *__CRTDECL operator new(size_t _Size, int nValue)
{
printf("%d %d\n", _Size, nValue);
return malloc();
}
int *p = new() int(); //p = 0x000e36b0 *p = 10; 输出4 20
//D:尽管在一般情况下我们可以自定义具有任何形参的operator new,但是下面这个函数却无论如何不能被用户重载:
inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()
{ // construct array with placement at _Where
return (_Where);
}
inline void *__CRTDECL operator new[](size_t, void *_Where) _THROW0()
{ // construct array with placement at _Where
return (_Where);
} inline void __CRTDECL operator delete(void *, void *) _THROW0()
{ // delete if placement new fails
}
inline void __CRTDECL operator delete[](void *, void *) _THROW0()
{ // delete if placement array new fails
}
// 上述的operator new 和 operator delete系列函数是供标准库调用的,所以不应该去重新定义
//E:对于operator delete系列的函数,它们的返回值类型必须是void,第一个参数必须是void*类型。执行一条delete表达式将调用相应的operator函数,并用待释放的指针来初始化void*形参。
// 当我们将operator delete系列的函数定义为类的成员时,该函数可以包含另一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。
// 若基类有一个虚析构函数,则传递给operator delete系列函数的字节数将因待删除指针所指对象的动态类型不同而有所区别,实际运行的operator delete函数的版本也会发生变化。
class CFather
{
public:
CFather(){}
virtual ~CFather() {printf("0\n");}
public:
void operator delete(void *p, size_t size) {free(p); printf("1 %d\n", size);}
public:
int value;
}; class CChild : public CFather
{
public:
CChild(){}
~CChild(){printf("2\n");}
public:
int value0;
int value1;
public:
void operator delete(void *p, size_t size) {printf("3 %d\n", size); free(p);}
}; CChild *pChild = new CChild;
CFather *pTem = pChild;
delete pTem; //输出2 0 3 16
//F:我们提供operator new 和operator delete系列函数的目的在于改变内存分配的方式,但是不管怎样,我们都无法改变new运算符和delete运算符的基本含义。
//G:在自己定义的operator new系列函数和operator delete系列函数中使用malloc和free(均定义在头文件cstdlib中)来完成内存的分配和释放。
//H:定位new形式(包含头文件new):将调用前面所述的不能被重新定义的new的版本
int *pInt = new int(); //调用标准库版本 pInt = 0x00251510 *pInt= 10
int *pInt1 = new(pInt) int(); //调用标准库版本 pInt1 = 0x00251510 *pInt1 = 50 *pInt = 50
// 当仅通过一个地址调用时,定位new使用operator new(size_t, void*);该函数不分配任何内存,仅仅是返回指针实参。具体定义和其对应的delete具体见条款D。
// 事实上,定位new允许我们在一个特定的、预先分配的内存上构造对象。这块内存甚至可以不是动态内存。
class CTest
{
public:
~CTest(){printf("0\n");}
};
CTest *Test0 = new CTest;
Test0->~CTest(); //调用析构函数会销毁对象但是并不释放内存
CTest *Test1 = new(Test0) CTest; //重用内存
delete Test1; //释放内存 //2.typeid运算符:
// A:typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。定义在头文件typeinfo中。
// B:typeid作用于表达式时,顶层const将被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。typeid作用于数组或者函数的时候,不会执行向指针的标准类型转换。
// C:当运算对象不属于类类型或是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。当运算对象是一个至少含有一个虚函数的类的左值的时候,typeid的结果直到运行时候才会求得。
// D:typeid的操作结果是一个常量对象的引用,该对象的类型是type_info类型或此类的公有派生类型。此类定义了 == != name()成员。其中name()成员返回一个C风格字符串,表示类型名字的可打印形式。 //3.枚举类型:
// A:枚举类型使得我们可以将一组整形常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。枚举属于字面常量类型。
// B:C++包含两种枚举:限定作用域的和不限定作用域的。其中vs2010只支持后者,所以这里不对前者进行研究。
// C:在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身作用域相同。
// D:默认情况下,枚举值从0开始,依次加一,不过也能为一个或几个枚举成员指定专门的值。
// E:枚举成员是const的,因此在初始化枚举成员的时候提供的初始值必须是常量表达式。
// F:只要enum有名字,我们就可以定义并初始化该类类型的成员。要想初始化enum对象或为其赋值,必须使用该类型的一个枚举成员或该类型的另一个对象。
// G:一个不限定作用域的枚举类型的对象或枚举成员可以自动的转化为整形。因此我们可以在任何需要整形值的地方使用它们。enum的值会提升为int或更大的整形。
// H:可以在enum的名字后面加上冒号以及我们想在该enum类型中使用的类型:
enum A : long long {a}; //4.类成员指针:
// A:成员指针是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是类的成员而非类的对象。类的静态成员不属于任何对象。因此无需特殊的指向静态成员的指针。指向静态成员的指针和一般指针没什么区别。
// B:成员指针的类型囊括了类的类型以及成员的类型。当初始化一个这样的指针的时候,我们令其指向类的某个成员,但不指定该成员所属的对象。直到使用成员指针时,才提供成员所属的对象。
// C:定义类成员指针的形式: int A::*p;则p是一个指向A类的int成员的指针。 p = &A::value;将&作用于A类的成员value而非内存中的对象。类对象可以使用.*和->*来操作类成员指针
class A
{
public:
A() : value(){}
public:
int value;
int GetValue(){return value;}
};
int A::*pValue = &A::value;
A a;
a.*pValue = ;
int v0 = a.value; //v0 = 20
(&a)->*pValue = ;
int v1 = a.value; //v1 = 10
auto A::*pGetValue = &A::GetValue; //除了定义指向类的数据成员的指针,还可以定义指向类的成员函数的指针
int v2 = (a.*pGetValue)(); //v2 = 10 //由于函数调用运算符的优先级高于指针指向成员运算符,所以这里的()不可少
// D:常规的访问控制对类成员指针同样有效。
// E:和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
// F:若类成员指针指向重载的函数,则必须显示的声明类成员指针类型以明确指出我们想要使用的是哪个函数。
// G:和其它函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型,并且指向成员的指针形参也可以拥有默认实参。
// H:由上可知,想要通过一个指向成员函数的指针进行函数调用,必须首先利用.*或->*运算符将该指针绑定到特定的对象上。因此,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。
// I:从指向成员函数的指针获取可调用对象的第一种方法是使用标准库模板function:
vector<string> svec;
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn); vector<string*> svec;
function<bool (const string*)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
// 注意点:由于指向成员函数的指针进行函数调用,必须首先利用.*或者.->来将该指针绑定到特定的对象上,所以上述两种的function的声明形式根据vector中存储的类型不同而不同
// J:第二种方法是使用标准库功能mem_fn(定义在头文件functional中,声明在std中)来让编译器负责推断成员的类型:
vector<string*> svec;
find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); vector<string> svec;
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
// K:第三种方法是使用bind
auto f = bind(&string::empty, _1);
vector<string> svec;
find_if(svec.begin(), svec.end(), f);
//5.嵌套类:
// A:一个类可以定义在另一个类的内部,前者称为嵌套类或嵌套类型。嵌套类通常用于定义作为实现部分的类。
// B:嵌套类是一个独立的类,与外层类基本没什么关系。外层类对象和嵌套类的对象是相互独立的。在嵌套类对象中不包含任何外层类定义的成员。在外层类中不包含任何嵌套类定义的成员。
// C:嵌套类的名字在外层作用域中是可见的,在外层作用域之外是不可见的。
// D:和成员函数一样,嵌套类必须声明在类的内部,但是可以定义在类的外部。在嵌套类在其外层类之外完成真正的定义前,它都是一个不完全类型。 //6.union:
// A:联合是一种特殊的类。一个union可以有多个数据成员,但是再在任意时刻只有一个数据成员可以有值。分配给一个union对象的存储空间至少要能容纳它的最大的数据类型。
// B:union提供了一种有效的途径使得我们可以方便的表示一组类型不同的互斥值。 //7.当对象的值可能在程序的控制或检测之外被改变,应该将此变量声明为volatile。
// volatile限定符的用法和const类似(作用于指针的时候,作用于成员函数的时候),起到对类型的额外修饰功能。 //8.链接指示:
// A:C++程序有时需要调用其他语言编写的函数,最常见的是调用C语言编写的函数。像所有其他名字一样,其他语言中的函数的名字也必须在C++中进行声明。
// B:对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通的C++函数相同,但是生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。
// C:链接指示可以有两种形式:单个的或符合的
extern "C" size_t strlen(const char*); //单一的
extern "C" //复合的
{
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
// D:extern可作用于头文件,头文件中所有普通函数声明都被认为是由链接指示的语言编写的:
extern "C"
{
#include<string.h>
}
// E:指向C函数的指针和指向C++函数的指针是不一样的类型。