本章介绍如何对类使用new和delete以及如何处理由于使用动态内存而引起的一些微妙的问题。
一个具体的例子——c++如何增加内存负载。假设要创建一个类,其一个成员表示某人的姓,最简单的方法是使用字符数组来保存姓,但初始化多大呢,40位?2000位?所有这时需要动态的在运行过程中创建合适长度的数组。通常的c++方法是,在类构造函数中使用new运算符在程序运行时分配所需的内存,还需要执行一些额外的操作:扩展类析构函数、使所有的构造函数与new析构函数协调一致、编写额外的类方法来帮助正确完成初始化和赋值。
12.1 动态内存和类
代码来源
类的声明,其中有一个成员是静态存储类 num_strings。静态类有一个特点,无论创建了多少对象,程序都只创建一个静态类变量副本(图片见书P427)。
//stringbad.h
#include<iostream>
#ifndef STRINGBAD_H
#define STRINGBAD_H
class StringBad
{
private:
char * str;
int len;
static int num_strings;//无论创建多少个StringBad对象,程序之创建一个静态类变量副本
//这样可以方便地说明数据成员属性,比如用来记录所创建对象的数目
public:
StringBad(const char * s);
StringBad(const StringBad & st);
StringBad();
~StringBad();
StringBad & StringBad::operator=(const StringBad & st);
friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};
#endif
该对象使用char指针(而不是char数组)来表示姓名,意味着类声明中没有为字符串本分分配存储空间,而是在构造函数中使用new来为字符串分配空间,这避免了在类声明中预先定义字符串的长度。
#include"stdafx.h"
#include<cstring>
#include"stringbad.h"
using std::cout;
int StringBad::num_strings = 0;
//静态数据成员在类声明中声明,在包含类方法的文件中初始化,初始化时使用作用域运算符来
//指出静态成员所属的类,但如果静态成员是const整数类型或者是枚举类型,则可以在声明中初始化
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\n";
}
StringBad::StringBad(const char * s)
{
len = std::strlen(s);
//strlen()返回字符串长度,但是并不包含末尾的空字符,因此分配内存长度为len+1
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
cout << "num_strings " << num_strings << ": " << str << "created!\n\n";
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
std::strcpy(str, "C++");
num_strings++;
cout << num_strings << " : " << str << "default object created!\n\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted,";
--num_strings;
cout << num_strings << " left\n\n";
delete [] str;
}
std::ostream & operator<<(std::ostream & os, const StringBad &st)
{
os << st.str;
return os;
}
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;
}
在由类生成对象时,构造函数分配足够的内存(通过传入的字符串的长度)来存储字符串,然后将字符串复制到内存中。这里要注意的是:这里生成的对象中,只保留了字符串的地址信息,字符串本身并不保存在对象中(而保存在由new开辟的堆内存中heap),之后即使你删除了对象,但字符串所占用的内存并不会自动释放,这就需要由析构函数中的delete来释放由new所申请的内存。
/ StringBad.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#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("Lecttuce Prey");
StringBad sports("Spinach Leaves Bow1 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\n";
StringBad sailor = sports;
//相当于运行了StringBad sailor = StringBad(sports);
//这里=sport无法调用正常的构造函数,因为sport不是char*也不是null,是一个String对象,这涉及到了**复制构造函数**
//在这里,编译器会自动调用复制构造函数StringBad(const StringBad &),从而使得
//结果运行异常,结绝的方法是定义复制构造函数StringBad(const StringBad &)
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n\n";
return 0;
}
cout << "End of main() \n\n";
}
void callme1(StringBad & rsb)
{
cout << "String passed by reference:\n\n";
cout << " \"" << rsb << "\"\n\n";
}
void callme2(StringBad sb)
{
cout << "String passed by value: \n\n";
cout << " \"" << sb << "\"\n\n";
}
以上程序有一些错误,导致无法编译通过,主要的原因在于
StringBad sailor = sports;
StringBad headline2("Lecttuce Prey");
void callme2(StringBad sb)
{
cout << "String passed by value: \n\n";
cout << " \"" << sb << "\"\n\n";
}
sport不是默认构造中的数据类型,而是一个StringBad对象,实现的是通过复制来赋值的操作,这里涉及到一个构造方法(也可能涉及一个对象赋值的操作),叫做 复制构造函数(系统有默认的,是浅层复制,当我们涉及到new来申请内存时,需要对复制构造函数进行修改)
12.1.2特殊成员函数
在定义一个类时,有一些成员函数是自动定义的,c++提供了下面的成员函数:
- 默认构造函数,如果没有定义构造函数;
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义
- 地址运算符,如果没有定义
结果表明,StringBad类中的问题是由隐式复制函数和隐式赋值运算符引起的。
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中,用于初始化过程中(所以才可以叫做构造函数)(包括按值传递参数和函数返回对象而非对象引用),而不是常规的赋值过程中。
类的复制构造函数原型通常如下,它接受一个指向类对象的常量引用作为参数。
StringBad(const StringBad & st);
- 何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,最常见的情况是将新对象显式地初始化为现有的对象(用按值传递时从实参到形参的复制过程也属于复制构造,构造调用函数中的形参的值;函数返回对象而非引用也会调用复制构造函数)
StringBad ditto(motto);
StringBad merro = metto;
StringBad also = StringBad(metto);
StringBad *p = new StringBad(metto);
第二种和第三种还可能涉及到赋值运算符的重载问题(也是造成浅层复制的原因之一),这两种过程可能直接调用了复制构造函数,或者可能使用复制构造函数先生成一个临时对象,然后将临时对象的内容赋值给merro或also。不过无论如何复制构造函数都将调用。
每当程序生成对象副本时,编译器都将使用复制构造函数。按值传递时,形参的初始化会调用复制构造函数;函数返回对象而非引用时,返回的对象将会调用复制构造函数将结果初始化到自己身上。
callme2(headline);
这里程序使用复制构造函数初始化sb——callme2()函数的StringBad型形参。
- 复制构造函数的功能
默认的构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。当值是数组时就完蛋了,由于只是复制值,两个变量会同时指向同一块地址,当第一个对象被析构时,第二个对象的数组就消失了。这就是浅复制造成复制构造函数出错的原因。我们对于带有地址的变量的复制需要开辟新的内存来复制,用这个思想来编写新的复制构造函数。
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";
}
赋值运算符的重载
默认的赋值运算符也是浅复制的,因此对其的修改如下,由于不涉及新建对象,所以num_strings不需要++
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;
}
这样所有的问题就解决了。
12.2改进string类
编辑了一天的没保存- =天哪!!