大发彩_票平台开发
地址一:【hubawl.com】狐霸源码论坛
地址二:【bbscherry.com】
类和动态内存分配
- 动态内存和类
C++在分配内存时是让程序在运行时决定内存分配,而不是在编译时决定。 这样,可根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存。C++使用new和delete运算符来动态控制内存。
1.1. 复习示例和静态类成员
这个程序使用了一个新的存储类型:静态类成员。
//strngbad.h
#include<iostream>
#ifndef STRNGBADH
#define STRNGBADH
class StringBad
{
private:
char str; //指向字符串的指针
int len; //字符串的长度
static int num_strings; //对象个数
public:
StringBad(const char s); //构造函数
StringBad(); //默认构造函数
~StringBad(); //析构函数
//友元函数
friend std::ostream & operator<<(std::ostream & os,
const StringBad & st);
};
#endif // !STRNGBADH
注意:
首先,它使用char指针(而不是char数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。 这避免了在类声明中预先定义字符串的长度。
其次,将num_strings成员声明为静态存储类。 静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。 也就是说,类的所有对象共享同一个静态成员。 假设创建了10个StringBad对象,将有10个str成员和10个len成员,但只有一个共享的num_strings成员。这对于所有类对象都具有相同值的类私有数据是非常方便的。 例如,num_strings成员可以记录所创建的对象数目。
//strngbad.cpp
#include<cstring>
#include"strngbad.h"
using std::cout;
//初始化静态类成员
int StringBad::num_strings = 0;
//类方法
//以C字符型构造StringBad
StringBad::StringBad(const char * s)
{
len = std::strlen(s); //设置长度
str = new char[len + 1]; //分配内存
std::strcpy(str, s); //初始化指针
num_strings++; //设置对象数量
cout << num_strings << ": \"" << str
<< "\" object created\n";
}
StringBad::StringBad() //默认构造函数
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); //默认字符串
num_strings++;
cout << num_strings << ": \"" << str << "\" default object created\n";
}
StringBad::~StringBad() //必要的析构函数
{
cout << "\"" << str << "\" object deleted, ";
--num_strings; //有要求
cout << num_strings << " left\n";
delete[] str; //有要求
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
注意: 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。 对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。
析构函数:str成员指向new分配的内存。当StringBad对象过期时,str指针也将过期。 但str指向的内存仍被分配,除非使用delete将其释放。 删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此,必须使用析构函数。 在析构函数中使用delete语句可确保对象过期时,由构造函数使用new分配的内存被释放。
//vegnews.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using std::cout;
#include"strngbad.h"
void callme1(StringBad &); //传递引用
void callme2(StringBad); //按值传递
int main()
{
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Lettuce Prey");
StringBad sports("Spinach Leaves Bowl for Dollars");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
callme2(headline2);
cout << "headline2: " << headline2 << endl;
cout << "Initialize one onject to another:\n";
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
std::cin.get();
return 0;
}
void callme1(StringBad & rsb)
{
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
程序开始时还是正常的,但逐渐变得异常,最终导致了灾难性结果。
首先来看正常的部分。 构造函数指出自己创建了3个StringBad对象,并为这些对象进行了编号,然后程序使用重载运算符<<列出这些对象:
1: "Celery Stalks at Midnight"object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Bowl forDollars" object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
然后,程序将headline1传递给callme1()函数,并在调用后重新显示headline1。 代码如下:
callme1(headline1);
cout<< "headline1: " << headline1 << endl;
下面是运行结果:
String passed by reference:
"Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
这部分代码看起来也正常。
但随后程序执行了如下代码:
callme2(headline2);
cout<< "headline2: " << headline2 << endl;
这里,callme2()按值(而不是按引用)传递headline2,结果表明这是一个严重的问题!
String passed by value:
"Lettuce Prey"
"Lettuce Prey" object deleted, 2left
headline2: 葺葺葺葺葺葺葺葺軮
将headline2作为函数参数来传递从而导致析构函数被调用。
另外,下面的代码:
StringBad sailor= sports;
这使用的是哪个构造函数呢?不是默认构造函数,也不是参数为const char*的构造函数。记住,这种形式的初始化等效于下面的语句:
StringBad sailor = StringBad(sports); //使用sports的构造函数
因为sports的类型为StringBad,因此相应的构造函数原型应该如下:
StringBad(const StringBad &);
当您使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一个副本)。 自动生成的构造函数不知道需要更新静态变量num_string,因此会将计数方案搞乱。 实际上,这个例子说明的所有问题都是由编译器自动生成的成员函数引起的。
1.1. 特殊成员函数
StringBad类的问题是由特殊成员函数引起的。 这些成员函数是自动定义的,就StringBad而言,这些函数的行为与类设计不符。 具体地说,C++自动提供了这些成员函数:
默认构造函数,如果没有定义构造函数;
默认析构函数,如果没有定义;
复制构造函数,如果没有定义;
赋值运算符,如果没有定义;
地址运算符,如果没有定义。
(1) 默认构造函数:
如果没有提供任何构造函数,C++将创建默认构造函数。 例如,加入定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:
Klunk::Klunk() { } //隐式默认构造函数
Klunk lunk; //调用默认构造函数
默认构造函数使Lunk类似于一个常规的自动变量,也就是说,它的值在初始化时是未知的。
如果定义了构造函数,C++将不会定义默认构造函数。 如果希望在创建对象时不显式地对它进行初始化,则必须显式地定义默认构造函数。这种构造函数没有参数,但可以使用它来设置特定地值:
Klunk::Klunk() //显式默认构造函数
{
klunk_ct=0;
…
}
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。 例如,Klunk类可以包含下述内联构造函数:
Klunk(int n = 0) {klunk_ct = n;}
但只能有一个默认构造函数。 也就是说,不能这样做:
Klunk() {klunk_ct = 0;} //构造函数#1
Klunk(int n =0) {klunk_ct=n;} //具有二义性的构造函数#2
例如:
Klunk kar(10); //明确地与#1匹配
Klunk bus; //与两个构造函数均可匹配
第二个声明既与构造函数#1(没有参数)匹配,也与构造函数#2(使用默认参数0)匹配。
(2) 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。 类的复制构造函数的原型如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。 例如,StringBad类的复制构造函数原型如下:
StringBad(const StringBad &);
对于复制构造函数,需要知道两点:何时调用和有何功能。
(3) 何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。 这在很多情况下都可能发生,最常见的情况是将新对象显式地初始化为现有地对象。例如,假设motto是一个StringBad对象,则下面4种声明都将调用复制构造函数:
StringBadditto(motto); //调用StringBad(const StringBad &)
StringBad metoo =motto; //调用StringBad(const StringBad &)
StringBad also = StringBad(motto); //调用StringBad(const StringBad &)
StringBad*pStringBad = new StringBad(motto);
////调用StringBad(constStringBad &)
其中中间的两种声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo和also,这取决于具体的实现。 最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。
每当程序生成了对象副本时,编译器都将使用复制构造函数。 由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。 这样可以节省调用构造函数的时间以及存储新对象的空间。
(4) 默认的复制构造函数的功能
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
StringBad sailor = sports;
与下面代码等价(由于私有成员是无法访问的,因此这些代码不能通过便于):
StringBad sailor;
sailor.str=sports.str;
sailor.len=sports.len;
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。 静态函数(如num_strings)不受影响,因为它们属于整个类,而不是各个对象。
1.2. 回到StringBad:复制构造函数的哪里出了问题
当callme2()被调用时,复制构造函数被用来初始化callme2()的形参,还被用来将对象sailor初始化为对象sports。 默认的复制构造函数不说明其行为,因此它不指出创建过程,也不增加计数器num_strings的值。但析构函数更新了计数,并且在任何对象过期时都将被调用,而不管对象是如何被创建的。 这是一个问题,因为这意味着程序无法准确地记录对象计数。 解决方法是提供一个对计数进行更新地显式复制构造函数:
StringBad::StringBad(const StringBad & s)
{
num_strings++;
...
}
第二个异常之处更微妙,也更危险:
headline2: 葺葺葺葺葺葺葺葺軮
原因在于隐式复制构造函数是按值进行复制的。 隐式复制构造函数的功能相当于:
sailor.str = sports.str;
这里复制的并不是字符串,而是一个指向字符串的指针。 也就是说,将sailor初始化为sports后,得到的是两个指向同一个字符串的指针。 当operator<<()函数使用指针来显示字符串时,这并不会出现问题。但当析构函数被调用时,这将引发问题。
(1) 定义一个显式复制构造函数以解决问题
解决类设计种这种问题的方法时进行深度复制(deep copy)。 也就是说,复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。 这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。 调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串。可以这样编写String的复制构造函数:
StringBad::StringBad(const StringBad & st)
{
num_strings++; //处理静态成员更新
len= st.len; //相同长度
str= new char[len + 1]; //分配空间
std::strcpy(str,st.str); //将字符串复制到新位置
cout<< num_strings << ":\"" << str
<< "\" object created\n";
}
必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
1.3. StringBad的其他问题:赋值运算符
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。 这种运算符的原型如下:
Class_name & Class_name::operator=(constClass_name &);
它接受并返回一个指向类对象的引用。 例如,StringBad类的赋值运算符的原型如下:
StringBad & StringBad::operator=(constStringBad &);
(1) 赋值运算符的功能以及何时使用它
StringBad headline1(“Celery Stalks atMidnight”);
…
StringBad knot;
Knot = headline1; //赋值运算符被调用
初始化对象时,并不一定会使用赋值运算符:
StringBad metoo=knot; //可能使用复制构造函数,也可能是赋值运算符
这里,metoo是一个新创建的对象,被初始化为knot的值,因此使用复制构造函数。 然而,正如前面指出的,实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=运算符时也允许调用赋值运算符。
(2) 解决赋值的问题
对于由于默认赋值运算符不合适导致的问题,解决办法时提供赋值运算符(进行深度复制)定义。 其实现与复制构造函数相似,但也有一些差别。
由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
函数应当避免将对象赋给自身;否则,对对象重新赋值前,释放内存操作可能删除对象的内容。
函数返回一个指向调用对象的引用。
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st) //对象赋值给自身
return *this; //结束
delete[] str; //释放老字符串
len= st.len;
str= new char[len + 1]; //为新字符串开辟空间
std::strcpy(str,st.str); //复制字符串
return *this;
}
如果地址相同,程序将返回*this,然后结束。 如果地址不同,函数将释放str指向的内存,这是因为稍后把一个新字符串的地址赋给str。 如果不首先使用delete运算符,则上述字符串将保留在内存中。赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。
原文地址:http://blog.51cto.com/13869630/2142548