通常,new负责在堆中找到一个足以满足要求的内存块。new运算符还有另一种变体,被称为定位new运算符。
功能:可以指定要在内存中的位置存放数据。
该内容简单,直接用一个小程序来说明即可:
#include
#include
using namespace std;
char buffer[5000];
int main()
{
double* p1, * p2; //先定义两个double类型的指针
p1 = new double[100]; //开辟一个内存空间:(1)大小为100个double (2)p1指向这个空间的起始位置
p2 = new(buffer)double[100]; //开辟一个内存空间: (1)大小为100个double (2)p2指向这个空间的起始位置 (3)这个内存空间从buffer的
//内存空间占用,且占用起始位置为buffer数组的首地址。
//可以输出他们的地址来求证
for (int i = 0; i < 100; i++) //先初始化赋值
{
p1[i] = p2[i] = i + 1;
}
cout << " buffer数组的首地址是:" << (void*)buffer << endl;
cout << "数组p1的首地址是:" << p1 << endl;
cout << "数组p2的首地址是:" << p2 << endl;
//结果说明buffer的地址和p2的地址是一样的。
//那么这就要注意了,虽然说p2数组是使用new运算符的,但是不能使用delete删除,因为本质上,p2是使用静态数组的空间,而不是从堆空间开辟的
delete[]p1;
return 0;
}
上面的程序说明了3点:
再加深一点
如果想要在buffer数组的中间位置新建一个数组,而不是在buffer的头位置新建数组,该怎么搞?
很简单,看下面的程序:
#include
#include
using namespace std;
char buffer[5000];
int main()
{
double* p1, * p2; //先定义两个double类型的指针
p1 = new double[100]; //开辟一个内存空间:(1)大小为100个double (2)p1指向这个空间的起始位置
p2 = new(buffer)double[100]; //开辟一个内存空间: (1)大小为100个double (2)p2指向这个空间的起始位置 (3)这个内存空间从buffer的
//内存空间占用,且占用起始位置为buffer数组的首地址。
//可以输出他们的地址来求证
for (int i = 0; i < 100; i++) //先初始化赋值
{
p1[i] = p2[i] = i + 1;
}
cout << " buffer数组的首地址是:" << (void*)buffer << endl;
cout << "数组p1的首地址是:" << p1 << endl;
cout << "数组p2的首地址是:" << p2 << endl;
//结果说明buffer的地址和p2的地址是一样的。
//那么这就要注意了,虽然说p2数组是使用new运算符的,但是不能使用delete删除,因为本质上,p2是使用静态数组的空间,而不是从堆空间开辟的
delete[]p1;
//delete[]p2 //error
//第二阶段;增加一个偏移量,从而灵活使用buffer空间
p2 = new(buffer + 100 * sizeof(double))double[100]; //可以这样理解:从buffer的头地址开始,依次移动100*8个字节,也就是buffer[800].那么
//也就是说,从buffer[800]开始,一直到buffer[800+100*8]的位置都是归p2所有(即使这里面有数据)
cout << endl << endl << "p2的地址是:" << p2 << endl;
cout << "buffer[800]的地址是:" << (void*)&buffer[800] << endl << "buffer的地址是:" << (void*)buffer << endl;
cout << " 会发现,p2的地址和buffer[800]是一样的,800这个偏移量没有算错,可能会发现地址只差了320,不是800,但是注意,地址是16进制!!\n";
return 0;
}
定位new运算符用于对象有一点问题。具体是什么;看一下下面的程序,有详细说明:
#include
#include
#include
using namespace std;
const int BUF = 512;
class JustTesting //搞一个类
{
private:
string words;
int number;
public:
JustTesting(const string& s = "Just Testing", int n = 0) //默认构造函数(只能有一个)
{
words = s;
number = n;
cout << "对象--"<<words << " 构建\n";
}
~JustTesting() //析构函数,里面加一个追踪功能
{
cout <<"对象--"<< words << " 析构\n";
}
void Show()const
{
cout << words << ", " << number << endl;
}
};
int main()
{
char* buffer = new char[BUF]; //使用常规new开辟对空间,buffer指向这个空间的首地址。等价于buffer[BUF],但是在堆空间中
JustTesting* pc1, * pc2; //两个对象指针
pc1 = new (buffer)JustTesting; //使用new的重载,定位new运算符
pc2 = new JustTesting("Heap1", 20); //常规new,开辟空间的同时调用构造函数初始化对象,然后让pc2指向这个对象
cout << " 内存空间地址:\n" << "buffer: " << (void*)buffer << endl << " pc2: " << pc2 << endl<<"pc1: "<<pc1<<endl;
cout << " 内存里的数据:\n";
cout << " pc1:";
pc1->Show();
cout << "pc2:";
pc2->Show();
cout << "上面的测试说明pc1不需要析构,也就是说,pc1的空间在buffer数组里面,还是自动变量,静态数组变量,不会调用析构函数。";
delete pc2;
return 0;
}
相信到这里,鉴于之前的重载复制函数和赋值运算符(上一篇博客C++P356-386),想到一个问题。如果说,使用定位new运算符导致对象不能析构,那么如果对象里面有字符串指针,那么岂不是不能释放空间了?
#include
#include
#include
#include
using namespace std;
const int BUF = 512;
class JustTesting
{
private:
int number;
public:
string words;
JustTesting(const string& s = "Just Testing", int n = 0) //默认构造函数(只能有一个)
{
words = s;
number = n;
cout << "对象--"<<words << " 构建\n";
}
~JustTesting() //析构函数,里面加一个追踪功能
{
cout <<"对象--"<< words << " 析构\n";
}
void Show()const
{
cout << words << ", " << number << endl;
}
};
int main()
{
char* buffer = new char[BUF]; //使用常规new开辟对空间,buffer指向这个空间的首地址。等价于buffer[BUF],但是在堆空间中
JustTesting* pc1, * pc2; //两个对象指针
pc1 = new (buffer)JustTesting; //使用new的重载,定位new运算符
pc2 = new JustTesting("Heap1", 20); //常规new,开辟空间的同时调用构造函数初始化对象,然后让pc2指向这个对象
cout << " 内存空间地址:\n" << "buffer: " << (void*)buffer << endl << " pc2: " << pc2 << endl<<"pc1: "<<pc1<<endl;
cout << " 内存里的数据:\n";
cout << " pc1:";
pc1->Show();
cout << "pc2:";
pc2->Show();
cout << "上面的测试说明pc1不需要析构,也就是说,pc1的空间在buffer数组里面,还是自动变量,静态数组变量,不会调用析构函数。\n\n\n";
JustTesting* pc3, * pc4;
pc3 = new(buffer)JustTesting("Bad idea", 6);
pc4 = new JustTesting("Heap2", 10);
cout << " 内存里的数据:\n";
cout << " pc3:";
pc3->Show();
cout << "pc4:";
pc4->Show();
delete pc2;
delete pc4;
cout << pc3->words<<endl;
pc3->~JustTesting(); //显示使用析构函数即可
delete[]buffer;
return 0;
}
总结:使用定位new运算符创建对象,那么要显示使用析构函数,否则对象不会被析构。这有什么坏处呢?暂时也不知道.好像释放buffer空间就相当于析构了对象了,毕竟对象是在buffer里面创建的。
要重新定义<<运算符,以便将它和cout一起用来显示对象的内容,要像下面这样:
ostream &operator<<(ostream &os,const c_name &obj) //c_name是类名
{
os<<...;
return os;
}
(1)如果该类提供了能够返回所需内容的公有方法,则可以在运算符函数中使用这种方法,这样就不需要设置为友元函数了。
(2)返回类型必须是引用
1.如果要将单个值转换为类类型,需要创建原型如下所示的类构造函数:
c_name(type_name value);
其中,c_name是类名,type_name是要转换的类型的名称。
2.如果要将类类型转换为其它类型,则需要创建原型如下的类成员函数
operator type_name();
即使该函数没有类型,但是仍然需要返回所需类型的值.
如果不想被隐式转换,应该使用关键字explicit;
如果类使用new运算符来分配类成员指向的内存,在设计时应采取一些预防措施
(1)嵌套结构和类
在类声明中声明的结构,类,或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象(当然,声明只是声明),只是说明了这个类型可以在类中使用。
如果在类的私有部分声明,则只能在该类中使用被声明的类型;
如果在类的公有部分声明,则可以从类的外部通过作用域解析运算符使用被声明的类型。
(2)类的常量成员初始化
如果在类中又const 的成员,则只能初始化,而不能赋值。所以对于const类型数据(和引用类型数据),必须在执行到构造函数体之前,即创建对象时初始化。C++提供了一种特殊的初始化方式,叫初始化列表(只有构造函数可以使用)。成员初始化列表由逗号分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后,函数体左括号之前。如果数据成员的名称为mdata,需要初始化为val,则初始化mdata(val)。
一个例子来说明初始化列表的使用和语法:
//class是一个类,a,b,c都是类的数据成员。则应该这样使用初始化列表
class ::class(int n,int m) :a(n), b(0), c(n*m)
{
...
}
上面的代码将a初始化为n,b初始化为0,c初始化为n*m;从概念上来说,这些工作都是创建对象的时候完成的,因为此时没有执行大括号里面的代码块。
注意几点: