1.复习new和delete以及学习静态类成员
// stringbad.h 表示一个正确完成了显而易见的工作,但是有一些有益的功能被省略了的类
#include<iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
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
num_strings是静态存储类,这种静态存储类的特点是:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象都共享同一个静态类成员。本程序中的num_strings表示的是所创建的对象数目。
// stringbad.cpp 类方法实现
#include <cstring>
#include "stringbad.h"
using std::cout;
//初始化静态类成员
int StringBad::num_strings = 0;
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 << "\" object created\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted, ";
--num_strings;
cout << num_strings << " left\n";
delete [] str;
}
friend std::ostream & operator<<(std::ostream & os,const StringBad & st)
{
os << st.str;
return os;
}
int StringBad::num_strings = 0;
这条语句将静态成员num_strings的值初始化为零,注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。初始化之所以在方法文件中进行,而不再头文件中进行,具体原因是因为如果在头文件中进行,可能会产生很多初始化副本,导致程序运行错误。
注意:const整数类型的和枚举型的静态数据成员可以在类中声明。
注意:字符串并不保存在对象中,而是单独保存在堆内存里,对象仅保存了指出到哪里去查找字符串的信息。
// vegnews.cpp
#include<iostream>
using std::cout;
#include"stringbad.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 object 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";
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";
}
注意:这个程序的输出结果是糟糕的,首先是调用析构函数之后,最后的剩余字符串数目结果不正确,其次是字符串的内容也遭到了破坏。字符串数目不正确的原因是系统默认提供了一个构造函数----复制构造函数,这也是*StringBad sailor = sports;*这条语句能够使用的原因,系统提供一个构造函数,格式为:*StringBad(const StringBad &);*当我们使用一个对象初始化另一个对象的时候就会调用这个默认构造函数,而在整个默认构造函数中,会导致new_strings不自增。字符串内容不正确的原因是,在按值传递内容的时候,系统会调用析构函数。
2.特殊成员函数
C++中会自动提供下列成员函数:
- 默认构造函数 如果没定义构造函数
- 默认析构函数 如果没定义
- 复制构造函数 如果没定义
- 赋值运算符 如果没定义
- 地址运算符如果没定义
注意:更加准确的说法是,编译器将生产上述最后三个函数的定义,如果程序使用对象的方式要求这样做。也就是如果您需要用一个对象去初始化另一个对象,则系统会提供复制构造函数。如果您要将一个对象的值赋值给另一个对象,系统会默认提供赋值运算符的定义。
- 默认构造函数
如果没有提供任何构造函数,C++将创建默认构造函数。例如,假如定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下述默认构造函数:
Klunk::Klunk();{ }
也就是说,编译器会提供一个不接受任何参数,也不进行任何操作的构造函数,该函数会在创建对象的时候调用。
如果显式的定义了构造函数,默认构造函数将不会被定义和使用,如果想要使用这种没有参数的构造函数,需要自行定义。
注意:默认构造函数使用的时候应该避免二义性,比如使用带有一个默认参数的构造函数和没有参数的构造函数同时使用,会造成问题。 - 复制构造函数
复制构造函数用于将一个对象赋值到一个新对象中,也就是说,它用于初始化的过程中,而非常规的赋值过程中(因为它是一个构造函数)。默认原型如下:
Klunk::Klunk(const Klunk &);
它接受一个指向类对象的常量引用作为参数。在新建一个对象并将其初始化为同类现有对象的时候,复制构造函数将被调用,最常见的情况是将一个新定义的对象显式的初始化为现有对象。例如下面的例子都将调用复制构造函数:(假设motto是一个StringBad类的对象)
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
*StringBad p = new StringBad(motto);
其中中间的两种声明方式可能使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,并且将临时对象的值赋值给metoo和also。最后一种声明方式是使用motto初始化一个匿名对象,并且将新对象的地址赋值给p指针。
注意:每当程序生成了对象副本的时候,编译器都会调用复制构造函数,也就是说,当函数按值传递对象或者函数返回对象时,都会使用复制构造函数。因为按值传递意味着要创建一个原对象的副本。编译器生成临时对象的时候也会使用复制构造函数,例如:(假设t是vector类型变量)
t = t1 + t2 + t3;
在这种情况下,可能会使用复制构造函数创建一个vector类型的中间变量存储中间结果。
默认的复制构造函数逐个复制非静态成员,复制的是成员的值,也就是浅复制。当然如果成员本身是指针,存储的内容是指向的地址,则复制构造函数复制的也是地址。
3.StringBad类的问题出在哪里
两个异常之处
- 程序输出表明,析构函数调用的次数要比构造函数调用的次数多了两次,原因可能是在使用callme2进行值传递的时候,以及对sailor进行赋值的时候调用了两次默认的复制构造函数,创建了两个对象,因为复制构造函数我们并没有显式定义,所以执行的是默认的操作,解决的方法是提供一个能够更新num_strings的复制构造函数。
StringBad::StringBad(const String & s)
{
num_strings++;
...//其他内容
}
- 第二个异常之处则更加的危险,症状是字符串的内容乱码,原因在于隐式复制构造函数是按值进行复制的,复制的内容并不是字符串,而是指针,内容是指向字符串的地址。也就是说,将sailor初始化为sport后,得到的是两个指向同一个字符串的指针。当operator<<()函数使用指针来显示字符串时,这并不会产生问题。但是当析构函数调用时,这将引发问题,析构函数~StringBad释放str指针指向的内存,因此释放sailor的效果如下:
delete [] sailor.str; //delete the string that ditto.str points to
sailor.str指针指向"Spinach Leaves Bowl for Dollars",因为他被赋值为sports.str。而释放sports的时候,则会出现如下效果:
delete [] sports.str; // effect is undefined
因为sports.str的内容已经被sailor的析构函数释放,再次释放将导致不确定,可能有害的后果。试图释放内存两次也可能会导致程序异常终止。
问题是出在复制构造函数上,所以解决问题也要从复制构造函数下手,我们需要显式定义一个复制构造函数:
//能够解决问题的复制构造函数
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";
}
注意:这种复制方法称为深复制,也就是说它是新创建了一个字符串,而非与原对象使用同一个地址指向的字符串,新对象和原对象所指向的字符串内容相同,地址不相同,也就是修改原字符串并不会对新字符串产生影响,反之亦然。
4.StringBad的其他问题:赋值运算符
C++所允许的类对象赋值,这是通过自动为类重载赋值运算符来实现的,这种运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &);
将已有的对象赋值给另一个对象,将使用重载的赋值运算符:
//使用重载的赋值运算符
StringBad headline1("Celery Stalks at Midnight");
StringBad knot;
knot = headline1;
与复制构造函数一样,赋值运算符的隐式实现也是对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响。
赋值出现的问题:与隐式复制构造函数相同,出现的问题都是会导致内存被释放两次,原因也是一样的,赋值运算符也属于浅复制,原对象和新对象共用同一个地址所指向的内容,所以当knot被释放之后,headline1所指向的内容就已经被销毁,再次释放headline1的时候就会出错。
解决方法如下:
- 由于目标对象可能引用了以前分配的数据,所以函数应该使用delete[]来释放这些数据
- 函数应当避免将对象赋给自身