从一个例子开始:
// 第一个文件
// 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
第二个文件:
// stringbad.cpp
#include <cstring>
#include "stringbad.h"
using std::cout;
using std::endl;
// 初始化类的静态成员
int StringBad::num_strings = 0;
// 构造函数
StringBad::StringBad(const char* s)
{
len = std::strlen(s);
str = new char(len + 1);
// 初始化指针str
std::strcpy(str, s);
num_strings++;
cout << num_strings << ": " << str << " object created" << endl;
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
std::strcpy(str, "C++");
num_strings++;
cout << num_strings << ": " << str << " default object created" << endl;
}
// 析构函数
// 这个是必要的, 因为在析构函数中对new申请的内存进行了释放
StringBad::~StringBad()
{
cout << str << " object deleted, ";
--num_strings;
cout << num_strings << " left" << endl;
delete[] str;
}
// 重载了<<运算符, 友元函数
std::ostream & operator<<(std::ostream & os, const StringBad &st)
{
os << st.str;
return os;
}
要注意下面这句话
int StringBad::num_strings = 0;
要注意不能在类声明中初始化静态成员变量(类声明位于头文件中, 程序可能将头文件包含在多个文件中, 如果在头文件中进行初始化, 将出现多个初始化语句副本, 但是如果静态成员是整型或者枚举类型const 则可以在类声明中初始化), 这是因为声明描述了如何分配内存, 但并不分配内存. 对于静态类成员, 可以在类声明之外单独使用语句来进行初始化, 这是因为静态类成员是单独存储的, 而不是对象的组成部分. 注意初始化语句指明了类型, 并用了作用域运算符, 但没有使用关键字static
之所以使用下面的语句:
str = new char[4];
std::strcpy(str, "C++");
而不用:
str = s;
是因为字符串并不保存在对象中, 字符串单独保存在堆内存中, 对象仅保存了指出到哪里去查找字符串的信息.
在构造函数中使用new来分配内存时, 必须在相应的析构函数中使用delete来释放内存. 如果使用new[]来分配内存, 则应使用delete[]来释放内存.
第三个文件:
// 第三个文件
// vegnews.cpp
// compile with stringbad.cpp
#include <iostream>
#include "stringbad.h"
using std::cout;
void callme1(StringBad &);
void callme2(StringBad);
int main()
{
using std::endl;
{
cout << "Starting an inner block." << endl;
StringBad headline1("the first sentence");
StringBad headline2("the second sentence");
StringBad sports("sports sentence");
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 ot another:" << endl;
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object ot another: " << endl;
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block" << endl;
}
cout << "End of main()" << endl;
return 0;
}
void callme1(StringBad & rsb)
{
cout << "String passed by reference:" << std::endl;
cout << " \"" << rsb << "\"" << std::endl;
}
void callme2(StringBad rsb)
{
cout << "String passed by value:" << std::endl;
cout << " \"" << rsb << "\"" << std::endl;
}
程序运行结果为:
从结果中可以看得出来, 程序没有按预期的运行, delete数据执行的过多, 最后程序也跑出了异常
下面分析出错的原因:
看下面这行代码:
StringBad sailor = sports;
这行代码并不会使用构造函数, 这种写法等效于:
StringBad sailor = StringBad(sports);
因为sports的类型为StringBad, 因此相应的构造函数原型如下:
StringBad(const StringBad &);
当我们使用一个对象来初始化另一个对象时, 编译器将自动生成上述构造函数(称为复制构造函数, 因为它创建对象的一个副本). 自动生成的构造函数不知道需要更新静态变量num_string, 因此会将计数搞乱.
特殊成员函数:
首先C++会自动为类提供下面这些成员函数:
1.默认构造函数, 如果没有定义构造函数
2.默认析构函数, 如果没有定义
3.复制构造函数(见上述的例子), 如果没有定义
4.赋值运算符, 如果没有定义
5.地址运算符, 如果没有定义
结果表明, StringBad类中的问题是由隐式复制构造函数和隐式复制运算符引起的.
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中, 也就是说, 它用于初始化过程中(包括按值传递参数), 而不是常规的赋值过程中, 类的复制构造函数原型通常如下:
Class_name(const Class_name &);
例如: StringBad类的复制构造函数原型如下:
StringBad(const StringBad &);
何时调用复制构造函数:
新建一个对象并将其初始化为同类现有对象时, 复制构造函数都将被调用. 这在很多情况下都可能发生, 最常见的情况是将新对象显示地初始化为现有的对象. 例如: 假设motto是一个StringBad对象, 则西面4中声明都将调用复制构造函数:
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);
其中中间的2中声明可能会使用复制构造函数直接创建metoo和alse, 也可能使用复制构造函数生成一个临时对象, 然后将临时对象的内容赋给metoo和also, 这取决于具体实现. 最后一种声明使用motto初始化一个匿名对象, 并将新对象的地址赋给pStringBad指针.
每当程序生成了对象副本时, 编译器都将使用复制构造函数. 具体说, 当函数按值传递对象或函数返回对象时, 都将使用复制构造函数. 按值传递意味着创建原始变量的一个副本, 编译器生成临时对象时, 也将使用复制构造函数.
例如上面的例子中的
callme2(headline2);
默认的复制构造函数的功能:
默认的复制构造函数逐个复制非静态成员(成员复制也成浅复制), 复制的是成员的值
在上面的StringBad类的例子中:
StringBad sailor = sports;
与下面的代码等效:
StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;
如果成员本身就是类对象, 则将使用这个类的复制构造函数来复制成员对象. 静态成员(如num_strings)不受影响, 因为他们属于整个类, 而不是对象.
上面那个StringBad类出问题的地方就是:
1.callme2()被调用的时候, 复制构造函数被用来初始化callme2()的形参
2.复制构造函数还被用来将对象sailor初始化为对象sports.
这两个地方导致没有正常调用num_strings的更新, 但是临时变量的析构函数仍然更新了计数.
解决办法是提供一个对计数进行更新的显示复制构造函数:
StringBad::StringBad(const StringBad & s)
{
...
num_string++;
...
}
提示:如果类中包含这样的静态数据成员, 即其值将在新对象被创建时发生变化, 则应该提供一个显示复制构造函数来处理计数问题.
另一处异常则更微妙, 也更危险
原因在于隐式复制构造函数是按值进行复制的, 例如StringBad例子中, 隐式复制构造函数的功能相当于:
sailor.str = sport.str;
这里复制的并不是字符串, 由于str是个char*, 所以是个指向字符串的指针, 因此当调用析构函数的时候delete[] sailor.str; 相当于将原本的字符数组的空间内容给释放了
完整的复制构造函数如下, 实现的是深度复制(deep copy), 也就是说, 复制构造函数应当复制字符串并将副本的地址赋给str成员, 而不仅仅是复制字符串地址.
StringBad::StringBad(const StringBad & st)
{
num_string++;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
cout << num_string << ": " << "Object created" << std::endl;
}
赋值运算符
StringBad的其他问题: 赋值运算符
C++允许类对象赋值, 这是通过自动为类重载赋值运算符实现的.
赋值运算符的原型如下:
Class_name & Class_name::operator=(const Class_name &);
StringBad的赋值运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &);
赋值运算符的功能以及何时使用它:
将已有的对象赋给另一个对象时, 将使用重载的赋值运算符:
StringBad headlin1("asdfljk");
...
StringBad knot;
// 使用赋值运算符
knot = headlin1;
// 这里metoo是一个新创建的对象, 被初始化Wieknot的值, 因此使用的是复制构造函数
// 也有可能会使用赋值运算符
StringBad metoo = knot;
与复制构造函数相似, 赋值运算符的隐式实现也是对成员进行逐个复制.
StringBad的例子中:
knot = headline1;
这个使用赋值运算符, 也是调用析构函数的时候会造成问题.
解决办法就是重载赋值运算符:
StringBad & StringBad::operator=(const StringBad & st)
{
// 函数应避免将对象赋值给自身: 否则, 给对象重新赋值前, 释放内存操作可能删除对象的内容.
if(this == &st)
return *this;
// 由于str中可能存在以前分配的数据, 因此函数应使用delete[] 来释放这些数据.
delete[] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
// 函数返回一个指向调用对象的引用.
return *this;
}