假设我们要创建一个类,其中有一个成员表示某人的姓,最简单的就是用字符串数组来保存,开始使用14个字符的数组,发现太小,保险的方法是使用40个字符的数组,但是当创建2000多个这个样的对象时,必定会造成内存浪费。通常使用string类,该类有良好的内存管理细节。但是这样就没有机会深入的学习内存管理了。
c++在内存分配方面,采用这样的策略,在程序运行时决定内存分配,而不是编译时决定。使用new和delete运算符进行动态控制内存,但在类中使用这些运算符将导致新的编程问题,这时候析构函数是必不可少的,而不再是可有可无的。有时候还必须重载赋值运算符。
复习new、delete和静态成员变量的工作原理:
首先我们设计一个string类stringbad,之所以bad是因为这个类存在比较多的问题。
//stringbad.h
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
class StringBad
{
private :
char *str;
int len;
static int num_strings;
public:
StringBad();
StringBad(const char * s);
~StringBad();
//friend
friend std::ostream &operator<<(std::ostream &os, const StringBad &st);
};
#endif
//stingbad.cpp
#include "stringbad.h"
int StringBad::num_strings = 0;
StringBad::StringBad()
{
len = 4;
str = new char[4];
std::strcpy(str, "c++");
num_strings++;
std::cout << "string create:" << str << " , " << num_strings << " object created" << std::endl;
}
StringBad::StringBad(const char * s)
{
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
std::cout << "string create:"<<str<<" , "<<num_strings << " object created" << std::endl;
}
StringBad::~StringBad()
{
num_strings--;
std::cout << "string delete:" << str << " , " << num_strings << " object left" << std::endl;
delete[]str;
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
有几点需要注意:
- 这个类使用了char指针成员,而不是char数组,意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间,这样避免的在类声明中预先定义了字符串的长度。
- num_strings声明为静态成员,静态成员的特点是:无论创建多少对象,程序都只创建一个静态成员类变量副本,即该类的所有对象共享一个静态成员。
int StringBad::num_strings = 0;
这条语句将静态成员num_strings初始化为0。注意,不能在类声明中初始化静态成员变量,因为声明只是描述了如何分配内存,但并不分配内存。对于静态成员,可以在类声明之外使用单独的语句进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句指明了类型,并使用了作用于运算符,但是没有使用关键字static。
静态成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来表示静态成员所属的类。但如果静态成员是const整数类型或者枚举时,则可以在类声明中初始化。
//mian.cpp
#include"stringbad.h"
void callme1(StringBad &s)
{
std::cout << "按引用传递" << std::endl;
std::cout << s << std::endl;
}
void callme2(StringBad s)
{
std::cout << "按值传递" << std::endl;
std::cout << s << std::endl;
}
void main()
{
std::cout << "---begin---" << std::endl;
{
StringBad A("the first string");
StringBad B("the second string");
StringBad C("the third string");
StringBad D;
StringBad E=A;
callme1(A);
std::cout << A << std::endl;
callme2(B);
std::cout << B << std::endl;
}
std::cout << "---end---" << std::endl;
}
编写上面的文件对生成的类进行测试,运行结果如下:
代码运行到callme1(A);没什么问题,关键就是下面的callme2(B);这个callme2函数是按值传递的。
将B作函数为参数来传递,从而导致析构函数被调用。虽然按值传递 可以防止原始参数被修改,但实际函数已使原始字符无法识别导致一些非法字符。
首先分析下程序:
-
创建了4个对象A B C D E ,其中
StringBad E=A;
没有使用我们定义的那两个构造函数,这时候上面的语句等效于下面的句子:
StringBad E=StringBad(A);
因此相应的构造函数的原型应是:
StringBad(const StringBad &);
当使用一个对象去初始化另一个对象时,自动成上述的构造函数,称为复制构造函数,该函数创建了一个对象的副本。自动生成的复制构造函数不知道需要更新num_strings -
调用按引用传递的函数callme2(B),为了不修改B,通过默认复制构造函数创建了一个B的副本,这时,副本和B的指针成员指向的是同一个地址,这个副本的作用于只限于这个函数内,函数结束时,将析构这个副本对象,但同时也将B的字符串指针也释放了,导致后面B析构出现问题,试图对释放过的内存进行释放操作是很危险的。
-
对象的析构顺序和创建顺序是相反的,正常的删除顺序应该是 E D C B A ,删除E D C是正常的,但是删除B出现了错误。程序终止,A没有析构成功,并没有执行最后一句。
特殊成员函数:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
上述程序的问题就是由隐式复制构造函数和隐式赋值运算符引起的。
默认构造函数,不接受任何参数,也不进行任何操作。
隐式地址运算符返回调用对象的地址,即this指针的值,这正是我们需要的。
复制构造函数:
用于复制一个对象到新创建的对象中,即用于初始化(包括按值传递参数),而不是常规的赋值过程中。
原型:Class_name (const Class_name &);//指向对象的常量引用作为参数
何时调用:
StringBad jack(luck);
StringBad jack=luck;
StringBad jack=StringBad(luck);
StringBad * jack=new StringBad(luck);
函数按值传递和返回对象.
默认复制构造函数
默认的赋值构造函数逐个赋值非静态成员(成员复制也称为浅复制)。
如果成员本身是其他类对象,则将使用这个类的复制构造函数来复制成员变量。
静态成员和静态变量不受影响,因为他们属于整个类而不是各个对象。
定义一个显式的复制构造函数和改进的析构函数解决上述问题:
StringBad::StringBad(const StringBad & s)
{
num_strings++;
len = s.len;
str = new char[len + 1];
std::strcpy(str, s.str);
std::cout << "string create:" << str << " , "
<< num_strings << " object created" << std::endl;
}
必须定义复制构造函数的原因在于:一些类成员是使用new初始化、指向指针的数据,而不是数据本身。
深复制和浅复制:深复制是复制指向的数据而不是指针,浅复制仅仅复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
赋值运算符:
重载赋值运算符:
原型:Class_name& Class_name ::operator =(const Class_name &);//接受并返回一个指向类对象的引用
何时调用:
- 将已有的对象赋给另一个对象时:
StringBad head(“this is a string”);
StringBad k;
k=head; - 初始化对象时,并不一定使用赋值运算符:
StringBad met=k;
新创建一个对象met,被初始化为k的值,因此使用复制构造函数。但是实现时可能分两步来处理:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。
也就是说,初始化总是调用复制构造函数,而使用=运算符也允许调用赋值运算符。
与复制构造函数相似,赋值运算符的隐式实现也是对每个成员进行逐个复制。如果成员本身是类对象,这使用该类的赋值运算符来复制该成员,但静态成员不受影响。
StringBad & StringBad::operator=(const StringBad & s)
{
if (this == &s)
return *this;
delete[]str;//重置原来的字符串
len = s.len;
str = new char[len + 1];
std::strcpy(str, s.str);
return *this;
}
与复制构造函数的一些差别:
- 由于目标对象可能引用了以前分配的数据,因此必须使用delete[]进行重置,释放这些数据
- 函数应该避免将对象赋给自己;否则,给对象重新赋值前,释放内存操作可能删除对象的内容
- 函数返回一个指向调用对象的引用,可以编写s0=s1=s2;这样的代码