C++-类和动态内存分配-进阶 -2
(1) 在构造函数中使用new时应注意的事项
注意:
a.如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
b.new和delete必须相互兼容。 new对应于delete,new[]对应于delete[]。
c.如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。 因为只有一个析构函数,所有的构造函数都必须与它兼容。 然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空。
d.应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。 通常,这种构造函数与下面类似。
String::String(const String & st)
{
num_strings++; //必要时处理静态成员
len=st.len; //与被复制的字符串长度相同
str = new char [len+1]; //分配空间
std::strcpy(str,st.str); //将字符串复制到新地址
}
具体地说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。 另外,还应该更新所有受影响的静态类成员。
e.应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。 通常,该类方法与下面类似:
String & String::operator=(const String & st)
{
if(this == &st) //对象赋值给自身
return *this; //完成
delete [] str; //释放旧字符串
len = st.len;
str = new char[len+1]; //为新字符串分配空间
std::strcpy(str,st.str); //复制字符串
return *this; //返回指向调用对象的引用
- 应该和不应该
下面的摘要包含了两个不正确的示例(指出什么是不应当做的)以及一个良好的构造函数示例:
String::String()
{
str=”default string”;
len=std::strlen(str);
}
String::String(const char * s)
{
len=std::strlen(s);
str=new char;
std::strcpy(str,s);
}
String::String(const String & st)
{
len=st.len;
str=new char[len+1]; //分配空间
std::strcpy(str,st.str); //复制值
}
第一个构造函数没有使用new来初始化str。 对默认对象调用析构函数时,析构函数使用delete来释放str。 对不是使用new初始化的指针使用delete时,结果将是不确定的,并可能是有害的。 可将该构造函数修改为下面任意一种形式:
String::String()
{
len=0;
str = new char[1];
str[0]=’\0’;
}
String::String()
{
len=0;
str=0; //在C++11中,也可以使用str=nullptr;
}
String::String()
{
static const char * s = “C++”; //仅初始化一次
len = std::strlen(s);
str = new char[len+1];
std::strcpy(str,s);
}
摘录中的第二个构造函数使用了new,但分配的内存量不正确。 因此,new返回的内存块只能保存一个字符。 试图将过长的字符串复制到该内存单元中,将导致内存问题。
第三个构造函数是正确的。
最后,下面的析构函数无法与前面的构造函数正常地协调工作:
String::~String()
{
delete str; //应该是delete [] str;
}
由于构造函数创建的是一个字符数组,因此析构函数应该删除一个数组。
(2). 有关返回对象的说明
- 返回指向const对象的引用
使用const引用常见原因是旨在提高效率。 例如,假设要编写函数Max(),它返回两个Vector对象中较大的一个:
Vector force1(50,60);
Vector force2(10,70);
Vector max;
max = Max(force1,force2);
下面两种实现都是可行的:
//版本1
const Vector Max(const Vector & v1, const Vector & v2)
{
if(v1.magval() > v2.magval())
return v1;
else
return v2;
}
//版本2
const Vector & Max(const Vector & v1, const Vector & v2)
{
if(v1.magval() > v2.magval())
return v1;
else
return v2;
}
这里有三点需要说明。 首先,返回对象将调用复制构造函数,而返回引用不会。 因此,第二个版本所做的工作更少,效率更高。 其次,引用指向的对象应该在调用函数执行时存在。 在这个例子中,引用指向force1和force2,它们都是在调用函数中定义的,因此满足这种条件。 第三,v1和v2都被声明为const引用,因此返回类型必须为const,这样才匹配。
2. 返回指向非const对象的引用
两种常见的返回非const对象情形是,重载赋值运算符以及重载与cout一起使用的<<运算符。
operator=()的返回值用于连续赋值:
String s1(“Good stuff”);
String s2,s3;
s3=s2=s1;
在上述代码中,s2.operator=()的返回值被赋给s3。 为此,返回String对象或String对象的引用都是可行的,但与Vector示例中一样,通过使用引用,可避免该函数调用String的复制构造函数来创建一个新的String对象。 在这个例子中,返回类型不是const,因为方法operator=()返回一个指向s2的引用,可以对其进行修改。
operator<<()的返回值用于串联输出:
String s1(“Good stuff”);
cout<<s1<<” is coming!”;
在上述代码中,operator<<(cout,s1)的返回值成为一个用于显示字符串”is coming!”的对象。 返回类型必须是ostream &。
3. 返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。 因此,当控制权回到调用函数时,引用指向的对象将不再存在。 在这种情况下,应返回对象而不是引用。 通常,被重载的算术运算符属于这一类。
Vector force1(50,60);
Vector force2(10,70);
Vector net;
net = force1 + force2;
返回的不是force1,也不是force2,force1和force2在这个过程中应该保持不变。 因此,返回值不能是指向在调用函数中已经存在的对象的引用。 相反,在Vector::operator+()中计算得到两个矢量的和被存储在一个新的临时对象中,该函数也不应返回指向该临时对象的引用,而应该返回实际的Vector对象,而不是引用:
Vector Vector::operator+(const Vector & b) const
{
return Vector(x+b.x,y+b.y);
}
(3). 使用指向对象的指针
上述程序使用数组索引值来跟踪最短的字符串和按字母顺序排在最前面的字符串。 另一种方法是使用指针指向这些类别的开始位置,下述程序使用两个指向String的指针实现了这种方法。 最初,shortest指针指向数组中的第一个对象。 每当程序找到比指向的字符串更短的对象时,就把shortest重新设置为指向该对象。 同样,first指针跟踪按字母顺序排在最前面的字符串。 这两个指针并不创建新的对象,而只是指向已有的对象。 因此,这些指针并不要求使用new来分配内存。
//sayings2.cpp -- 使用指向对象的指针
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdlib>
#include<ctime>
#include"string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
using namespace std;
String name;
cout << "Hi, what's your name?\n";
cin >> name;
cout << name << ", please enter up to " << ArSize
<< " short sayings <empty line to quit>:\n";
String sayings[ArSize]; //对象数组
char temp[MaxLen]; ////临时字符串存储
int i;
for (i = 0; i < ArSize; i++)
{
cout << i + 1 << ": ";
cin.get(temp, MaxLen);
while (cin&&cin.get() != '\n')
continue;
if (!cin || temp[0] == '\0') //空行?
break; //i不自增
else
sayings[i] = temp; //重载赋值
}
int total = i; //读到行数总和
if (total > 0)
{
cout << "Here are your sayings:\n";
for (i = 0; i < total; i++)
cout << sayings[i][0] << ": " << sayings[i] << endl;
//利用指针记录最短,按字母顺序排在最前面的字符串
String * shortest = &sayings[0]; //初始化为第一个对象
String * first = &sayings[0];
for (i = 1; i < total; i++)
{
if (sayings[i].length() < shortest->length())
shortest = &sayings[i];
if (sayings[i] < *first) //比较值
first = &sayings[i]; //地址赋值
}
cout << "Shortest saying:\n" << *shortest << endl;
cout << "First alphabetically:\n" << *first << endl;
srand(time(0));
int choice = rand() % total; //利用随机数找到脚标
//使用new来创建、初始化新的字符串对象
String * favorite = new String(sayings[choice]);
cout << "My favorite saying:\n" << *favorite << endl;
delete favorite;
}
else
cout << "Not much to say, eh?\n";
cout << "Bye.\n";
system("pause");
return 0;
}
程序运行结果:
Hi, what's your name?
Ezra
Ezra, please enter up to 10 short sayings <empty line to quit>:
1: a friend in need is a friend indeed
2: neither a borrower nor a lender be
3: a stitch in time saves nine
4: a niche in time saves stine
5: it takes a crook to catch a crook
6: cold hands, warm heart
7:
Here are your sayings:
a: a friend in need is a friend indeed
n: neither a borrower nor a lender be
a: a stitch in time saves nine
a: a niche in time saves stine
i: it takes a crook to catch a crook
c: cold hands, warm heart
Shortest saying:
cold hands, warm heart
First alphabetically:
a friend in need is a friend indeed
My favorite saying:
cold hands, warm heart
Bye.
- 注意: String * favorite = new String(sayings[choice]);
此代码使用new来为整个对象分配内存。 然后,当程序不再需要该对象时,使用delete删除它。 对象时当个的,因此,程序使用不带中括号的delete。 这将只释放用于保存str指针和len成员的空间,并不释放str指向的内存,而该任务将由析构函数来完成。
在下述情况下析构函数将被调用:
- 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
- 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
- 如果对象是用new创建的,则仅当显示使用delete删除对象时,其析构函数才会被调用。
2. 指针和对象小结
- 使用常规表示法来声明指向对象的指针:
String * glamour;
2. 可以将指针初始化为指向已有的对象:
String * first = & sayings[0];
3.可以使用new来初始化指针,这将创建一个新的对象:
String * favorite = new String(sayings[choice]);
4. 对类使用new将调用相应的类构造函数来初始化新创建的对象:
//调用默认构造函数
String * gleep = new String;
//调用构造函数:String(const char *)
String * glop = new String (“my my my”);
//调用构造函数:String(const String &)
String * favorite = new String(sayings[choice]);
5. 可以使用->运算符通过指针访问类方法:
if(sayings[i].length() < shortest->length())
6. 可以对对象指针应该解除引用运算符(*)来获得对象:
if(sayings[i]<*first) //比较对象值
first = &sayings[i]; //给对象地址赋值
3. 定位new运算符
定位new运算符能够在分配内存时指定内存位置。 将这种运算符用于对象时情况有些不同。
//placenew1.cpp -- new, 定位new, 无delete
#include<iostream>
#include<string>
#include<new>
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 << " constructed\n";
}
~JustTesting()
{
cout << words << " destroyed\n";
}
void Show() const
{
cout << words << ", " << number << endl;
}
};
int main()
{
char *buffer = new char[BUF]; //活动一块内存
JustTesting *pc1, *pc2;
pc1 = new(buffer)JustTesting; //将对象置入buffer
pc2 = new JustTesting("Heap1", 20); //将对象置入堆上
cout << "Memory block addresses:\n" << "buffer: "
<< (void *)buffer << " heap: " << pc2 << endl;
cout << "Memory contents:\n";
cout << pc1 << ": ";
pc1->Show();
cout << pc2 << ": ";
pc2->Show();
JustTesting *pc3, *pc4;
pc3 = new(buffer)JustTesting("Bad Idea", 6);
pc4 = new JustTesting("Heap2", 10);
cout << "Memory contents:\n";
cout << pc3 << ": ";
pc3->Show();
cout << pc4 << ": ";
pc4->Show();
delete pc2; //释放Heap1
delete pc4; //释放Heap2
delete[] buffer; //释放buffer
cout << "Done\n";
return 0;
}
程序运行结果:
Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 000E8B00 heap: 000D7818
Memory contents:
000E8B00: Just Testing, 0
000D7818: Heap1, 20
Bad Idea constructed
Heap2 constructed
Memory contents:
000E8B00: Bad Idea, 6
000DFBF8: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Done
该程序使用new运算符创建了一个512字节的内存缓冲区,然后使用new运算符在堆中创建两个JustTesting对象,并试图使用定位new运算符在内存缓冲区中创建两个JustTesting对象。 最后,使用delete来释放使用new分配的内存。
- 对于定位new运算符。 要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个内存单元不重叠。 例如:
pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof (JustTesting)) JustTesting(“Better Idea”,6);
2. 如果使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。
delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。 这种问题的解决方案是,显式地为使用定位new运算符创建的对象调用析构函数。
pc3-> JustTesting(); //销毁pc3指向的对象
pc1-> JustTesting(); //销毁pc1指向的对象
下述程序对定位new运算符使用的内存单元进行管理,加入到合适的delete和显式析构函数调用。 需要注意的一点是正确的删除顺序。 对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。 原因在于,晚创建的对象可能依赖于早创建的对象。 另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。
//placenew2.cpp -- new, 定位new, 无delete
#include<iostream>
#include<string>
#include<new>
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 << " constructed\n";
}
~JustTesting()
{
cout << words << " destroyed\n";
}
void Show() const
{
cout << words << ", " << number << endl;
}
};
int main()
{
char *buffer = new char[BUF]; //活动一块内存
JustTesting *pc1, *pc2;
pc1 = new(buffer)JustTesting; //将对象置入buffer
pc2 = new JustTesting("Heap1", 20); //将对象置入堆上
cout << "Memory block addresses:\n" << "buffer: "
<< (void *)buffer << " heap: " << pc2 << endl;
cout << "Memory contents:\n";
cout << pc1 << ": ";
pc1->Show();
cout << pc2 << ": ";
pc2->Show();
JustTesting *pc3, *pc4;
//修正定位new的地址
pc3 = new(buffer+sizeof(JustTesting))JustTesting("Bad Idea", 6);
pc4 = new JustTesting("Heap2", 10);
cout << "Memory contents:\n";
cout << pc3 << ": ";
pc3->Show();
cout << pc4 << ": ";
pc4->Show();
delete pc2; //释放Heap1
delete pc4; //释放Heap2
//显式销毁定位new对象
pc3->~JustTesting(); //销毁pc3指向的对象
pc1->~JustTesting(); //销毁pc1指向的对象
delete[] buffer; //释放buffer
cout << "Done\n";
return 0;
}
程序运行结果:
Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 0099DDC8 heap: 0099BDE0
Memory contents:
0099DDC8: Just Testing, 0
0099BDE0: Heap1, 20
Bad Idea constructed
Heap2 constructed
Memory contents:
0099DDE8: Bad Idea, 6
00994700: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Bad Idea destroyed
Just Testing destroyed
Done
该程序使用定位new运算符在相邻的内存单元中创建两个对象,并调用了合适的析构函数。