c++学习笔记——类和动态内存分配

 在类中使用动态内存分配的原因:避免占用太多内存空间,在程序运行时(而不是在编译时)确定要使用多少内存,最常用的方法就是在类构造函数中使用new运算符在程序运行时分配所需的内存。

动态内存和类

让程序在运行时决定内存分配,而不是在编译时决定,这样可根据程序的需要,而不是一系列严格的存储类型规则来使用内存。在类中使用动态内存分配,可能会导致很多新问题,首先就是析构函数时必不可少的,有时还必须重载运算符,以保证程序可以正常运行。

复习示例和静态类成员

下面将用一个示例来了解new 、delete和静态类成员的工作原理:

头文件:

// strngbad.h -- flawed string class definition
#include <iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
    char * str;                // 这里使用char指针(而不是char数组)来表示姓名,
//这就意味着类声明中没有为字符串分配内存空间,而是在构造函数中是使用new来为字符串分配空间。
    int len;                   // length of string
    static int num_strings;    // 这里将num_strings成员声明为静态存储类,
//后续无论程序创建了多少个对象,程序就只创建一个静态类变量副本,所有类都共享一个静态成员。
public:
    StringBad(const char * s); // constructor
    StringBad();               // default constructor
    ~StringBad();              // destructor
// friend function
    friend std::ostream & operator<<(std::ostream & os, 
                       const StringBad & st);
};
#endif

类的实现细节:

// strngbad.cpp -- StringBad class methods
#include <cstring>                    // string.h for some
#include "strngbad.h"
using std::cout;

// initializing static class member
int StringBad::num_strings = 0;

// class methods

// construct StringBad from C string
StringBad::StringBad(const char * s)
{
    len = std::strlen(s);             // set size
    str = new char[len + 1];          // allot storage
    std::strcpy(str, s);              // initialize pointer
    num_strings++;                    // set object count
    cout << num_strings << ": \"" << str
         << "\" object created\n";    // For Your Information
}

StringBad::StringBad()                // default constructor
{
    len = 4;
    str = new char[4];
    std::strcpy(str, "C++");          // default string
    num_strings++;
    cout << num_strings << ": \"" << str
         << "\" default object created\n";  // FYI
}

StringBad::~StringBad()               // necessary destructor
{
    cout << "\"" << str << "\" object deleted, ";    // FYI
    --num_strings;                    // required
    cout << num_strings << " left\n"; // FYI
    delete [] str;                    // required
}

std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
    os << st.str;
    return os; 
}

其中语句int StringBad ::num_strings = 0;解释如下:

这条语句的格式:这条初始化语句指出了类型,并且使用了作用域运算符,但是没有使用关键字static。

这条语句将静态成员num_strings的值初始化成0,注意不能在类声明中初始化静态成员变量,这是因为声明描述如何分配内存,但是并不分配内存。可以使用该种格式来初始化内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,原因是静态类成员是单独存储的,而不是对象的组成部分。

注意:初始化是在方法文件(实现细节)中,而不是在类声明(头文件)中,这是由于类声明位于头文件中,程序可能将头文件包含在其他几个文件中,如果在头文件中进行初始化,将出现多个初始化语句副本,进而引发错误。

静态数据成员在类声明(头文件)中声明,在包含类方法的文件(实现细节)中初始化,初始化时使用作用域运算符来指出静态成员所属的类,但是如果静态成员是const整数类型或者枚举类型,则可以在类声明中初始化。

num_strings++:

每个构造函数(函数原型是:StringBad(const char * s);  StringBad(); )中都有该语句,这就确保了每创建一个对象,共享变量num_strings的值都加一。

num_strings--:

每个析构函数都有这条语句,因此Stiring类将可以跟踪对象被删除的情况。

第一个构造函数:

StringBad::StringBad(const char * s)
{
    len = std::strlen(s);             // set size
    str = new char[len + 1];          // allot storage
    std::strcpy(str, s);              // initialize pointer
    num_strings++;                    // set object count
    cout << num_strings << ": \"" << str
         << "\" object created\n";    // For Your Information
}

其中,类成员str是一个指针,使用strlen()函数来计算字符串的长度,并将其记录在int变量len中,然后使用new分配足够的空间来保存字符串,然后将新内存的地址赋给str成员。(这里的len+1是是由于strlen函数()返回字符串的长度,但是不包含末尾的空字符,所以加一,使得分配的内存可能够存储包含空字符的字符串)。

语句std::strcpy(str, s);将字符串复制到新的内存中去。num_strings++更新对象计数更新。最后,构造函数显示当前的对象数目和当前对象存储的字符串。

这里需要理解的是:字符串保存并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。

语句strcpy(str, s);不能用str=s来代替,因为这样就是保存了地址,但是没有创建字符串副本。如果是str = s 的话,指针str和指针s指向的同一个内存地址和字符串,不是副本(地址一样就不是副本了,因为一点该地址被释放,数据就不存在了)。这条语句上面的str  = new char[len + 1]使用了动态内存分配开辟了一个新的内存,并将该新内存的地址赋给str指针变量,再使用strcpy()函数创建s的副本。

析构函数:

StringBad::~StringBad()               // necessary destructor
{
    cout << "\"" << str << "\" object deleted, ";    // FYI
    --num_strings;                    // required
    cout << num_strings << " left\n"; // FYI
    delete [] str;                    // required
}

解读:其中delete语句至关重要,str成员指向new分配的内存。当StringBad对象过期时,str指针也就过期了。但是str指向的内存依然被分配,除非使用delete将其释放。也就是删除str对象可以删除对象本身所占的内存,但是并不能自动释放str指向的内存,所以必须使用析构函数,在析构函数中使用delete语句可确保对象过期时,由构造函数使用new分配的内存被释放。

注意:在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[ ](包括中括号)来分配内存,则应使用delete[ ](包括中括号)来释放内存。

主函数文件(程序12.3):

// vegnews.cpp -- using new and delete with classes
// compile with strngbad.cpp
#include <iostream>
using std::cout;
#include "strngbad.h"

void callme1(StringBad &);  // pass by reference
void callme2(StringBad);    // pass by value

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");//创建了3个对象
        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;  //使用了sports初始化了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";
}

 输出为:

Starting an inner block.
1: "Celery Stalks at Midnight" object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Bowl for Dollars" object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
String passed by reference:
    "Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
String passed by value:
    "Lettuce Prey"
"Lettuce Prey" object deleted, 2 left
headline2: DÛ°
Initialize one object to another:
sailor: Spinach Leaves Bowl for Dollars
Assign one object to another:
3: "C++" default object created
knot: Celery Stalks at Midnight
Exiting the block.
"Celery Stalks at Midnight" object deleted, 2 left
"Spinach Leaves Bowl for Dollars" object deleted, 1 left
"Spinach Leaves Bowl for Dollars" object deleted, 0 left

对于语句 callme2(headline2);调用的是callme2( )函数,它是按值传递的,将headline2作为函数参数来传递会导致析构函数被调用。虽然按值传递可以防止原始数据被修改,但是实际上函数已使原始字符串无法识别,导致显示一些标准字符。

从输出结果来看,有很多问题,这些问题都是由编译器自动生成的成员函数引起的。 下面将介绍类的默认成员函数。

特殊的成员函数

对于StringBad类来说,c++提供了下面这些成员函数:

(1)默认构造函数,如果没有定义构造函数;

(2)默认析构函数,如果没有定义;

(3)复制构造函数,如果没有定义;

(4)复制运算符,如果没有定义;

(5)地址运算符,如果没有定义。

复制构造函数

作用:复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中(包含按值传递参数),而不是常规的赋值过程中。

它的原型一般是下面这样的:

Class_name (const Class_name &);

它接收一个指向类对象的常量引用作为参数。

何时调用复制构造函数

新建一个对象并将其初始化成同类现有对象时,复制构造函数就会被调用,最常见的情况就是将新对象显式地初始化成现有的对象。例如,下面四种情况都是将调用复制构造函数:

StringBad ditto(motto);        //其中motto是现有的
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motto);

其中,中间两种声明可能会直接调用复制构造函数直接创建metoo和also,也可能是使用复制构造函数生成一个临时对象,然后将临时对象的内容复制给metoo和also。这个取决于实现。最后一种声明使用初始化一个匿名变量,并将新对象的地址赋值给pstring指针。

当函数按值传递对象或者函数返回对象时,都将使用复制构造函数。例如上面程序中语句:

callme2(headline);

程序使用复制构造函数初始化sb——callme2()函数的StringBad()型形参。由于按值传递对象将调用复制构造函数,所以应该按引用传递对象,这样可以节省调用构造函数的时间或者存储新对象的空间。

默认的复制构造函数的功能

默认构造函数会逐个复制非静态成员(成员复制也称为浅复制)复制的是成员的值。

在上面的程序中

StringBad sailor = sports;

等效于下面(这里是助于理解的,由于私有成员无法访问,所以下面的代码实际上无法通过编译):

StringBad sailor;
sail.str = sports.str;
sailor.len = sports.len;

如果成员本身就是类对象,则使用这个类的复制构造函数来复制成员对象。静态函数不受影响,因为它们属于整个类,而不是各个对象。隐式构造函数的操作如下:

由上图可以看出是逐个复制成员,其中motto是现有的对象,ditto是使用复制构造函数新建的对象。可以看出ditto.str和ditto.str指向的是同一个字符串(指向同一个地址)

这里是我从网上找来的解释:

值传递时是把对象的拷贝而不是本身传递给函数,函数中对参数对象的任何修改都不会影响调用该函数的对象本身;关键是数据里如果是包含指针的话(这里的字符串成员str就是指针),要注意:因为按值传递对象的话,会调用拷贝函数,逐成员复制,从而复制指针(浅拷贝)就相当于(格式是一个指针变量=另一个指针变量),指针直接赋值两个指针指向的同一个内存地址, 相当于还是使用原来的数据,而拷贝的函数析构的时候会释放相同位置的内存的。而成员len因为不是指针,所以motto.len和ditto.len互不影响。

回到上面有问题的程序中,第一个问题是析构函数比构造函数的调用次数多了两次,这是因为使用默认复制构造函数另外创建了两个对象,也就是语句StringBad sailor = sports; 和StringBad knot;knot = headline1;但是默认的复制构造函数中并不更新num_strings的值。

第二个问题就是字符串出现乱码,也就是输出结果中的headline2: DÛ°;原因是语句 callme2(headline2);调用了函数callme2()函数,而函数callme2()是按值传递参数的,就是将headline对象的值传递给函数callme()的形参sb,相当于headline = sb,然后按值就是逐成员复制,正如上面的解释中所说的类成员中有指针(这里是str),会导致headline.str和sb.str指向的地址相同,但是sb是一个局部变量,作用域是这个函数内部,但是callme(headline2)这个语句执行完毕后,会调用析构函数释放sb.str指向的内存,但是由于headline2和sb.str指向的是同一个内存,所以当执行 cout << "headline2: " << headline2 << endl;对hendline2进行输出时会出现乱码的现象。

同理,将sailor初始化成sports后,得到的是两个指向同一个字符串的指针,也就是sailor.str和sports.str指向的同一块地址,析构函数StringBad释放str指针指向的内存,例如释放sailor的效果如下:

delete [] sailor.str;

sailor.str指针指向的“Spinach Leaves Bowl for Dollars” 占用的内存就会被释放。此时如果再执行语句delete [] sports;就会导致一些不确定的后果(这里的输出没有问题,但是再一些老的实现中会出现乱码字符的现象,但是问题时一样的,内存管理不善)。

解决方案——定义一个显式复制的构造函数

解解决类设计中这种问题的方法是进行深度复制。深度复制的定义:复制指向的数据,而不是指针。而浅复制只是复制指针,浅浅地复制一下指针信息,而不会深入挖掘以复制指针引用的结构。

也就是说复制构造函数应当复制字符串并将副本的地址给str成员(必须定义复制构造函数的原因,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身,是浅复制的话会出现多个指针指向同一个内存的现象,调用析构函数可能会出现错误),而不仅仅是复制字符串的地址。这样一来,每个对象都有自己的字符串,不会出现多个指针指向同一个字符串地址的现象。当调用析构函数时将释放不同的字符串,而不会试图去释放已经被释放的字符串。以StringBad类为例,可以这样编写它的复制构造函数:

StringBad::StringBad(const String & st)
{
    num_strings++;
    len = st.len;
    str = new char [len + 1];
    std::strcpy(str, st.str);
    cout << num_strings << ":\"" << str
        << "\" object created\n";
}

上面这个图就是对深度复制的图解,可以看到motto的成员str指向的字符串的地址是2400;而ditto的成员str指向的地址是字符串的副本地址2432。

StringsBad类的第三个问题,赋值运算符

上面程序出现的问题不仅仅是默认赋值构造函数的问题,还需要再看看默认赋值运算符。c++允许对类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:

Class_name & Class_name::operator = (const Class_name &);

它接受并返回一个指向类对象的引用。

所以,StingsBad类的赋值运算符的原型如下:

StringsBad & StringsBad:: operator = (const StringsBad &);

何时使用赋值赋值运算符

(1)将一个对象赋给另一个对象时,将使用重载的赋值运算符,例如上面主函数文件中的语句

StringBad knot;
knot = headline1;

(2)但初始化对象时,并不一定使用赋值运算符,例如下面这条语句:

StringBad metoo = knot;

其中,metoo是一个新建的对象,被初始化成为knot的值,使用的是复制构造函数(不同于第一种情况,第一种情况是首先使用默认构造函数创建一个对象knot,在使用重载的运算符进行赋值)。这里需要注意的是有的实现也可能分成两步来处理这条语句,使用赋值构造函数创建一个临时对象(等号右边,创建一个knot的临时拷贝),再通过赋值将临时对象的值赋值到新对象中。也就是说,不管是什么实现,总会调用复制构造函数,但是不一定会调用赋值运算符

赋值运算符的隐式实现也是对成员进行逐个复制。同样也会到像问题1和2那样的情况出现,解决方案就是提供赋值运算符的定义,实现过程与复制构造函数相似,但是也有些许的不同。如果只有一个返回对象,函数可以像常规赋值操作那样,当连续赋值,情况会有所不同,例如,如果S0、S1、S2都是StringsBad对象

S1 = S2 = S3;

上述的代码可转化成下面这样:

S0.operator = (S1.operator = (S2));

S1.operator = (S2)的返回值是S0.operator()的参数,因此重载运算符函数的返回是一个指向StringsBad的引用,参数类型正确。具体的实现过程如下:

String & 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指针,,所有的类方法都将this指针设置为调用它的对象的地址),它首先通过查看赋值运算符右边的地址(&s)是否与调用函数的对象(this)的地址相同来完成的,如果相同,程序返回*this,然后结束。

如果地址不同,那么函数将释放str(调用函数的那个对象)指向的内存,这是因为稍后会把一个新字符串的地址赋给str。如果不使用delete运算符,则上述的字符串就是保留在内存中,由于程序不再包含指向该字符串的指针,那么该内存就会被浪费掉。接下来的操作就是与前面的复制构造函数相相似,最后完成操作后,返回*this结束。这里需要留意的一点是赋值操作并不会创建新的对象,因此并不需要调整静态变量num_strings的值。经过上面的改进后,类StringBad就被“修复”l。

改进后的String类:

首先头文件(程序12.4

// string1.h -- fixed and augmented string class definition

#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;

class String
{
private:
    char * str;             // pointer to string
    int len;                // length of string
    static int num_strings; // number of objects
    static const int CINLIM = 80;  // cin input limit
public:
// constructors and other methods
    String(const char * s); // constructor
    String();               // default constructor
    String(const String &); // copy constructor
    ~String();              // destructor
    int length () const { return len; } //第一个新方法返回存储的字符串的长度
// overloaded operator methods    
    String & operator=(const String &);
    String & operator=(const char *);
    char & operator[](int i);          //以数组表示法访问字符串中各个字符的功能
    const char & operator[](int i) const;
// overloaded operator friends
    //使用三个友元函数对输入的字符串进行比较
    friend bool operator<(const String &st, const String &st2); 
    friend bool operator>(const String &st1, const String &st2);
    friend bool operator==(const String &st, const String &st2);
    //operator<<()函数提供简单的输入功能
    friend ostream & operator<<(ostream & os, const String & st);
    friend istream & operator>>(istream & is, String & st);
// static function
    static int HowMany();//补充静态类数据成员num_strings
};
#endif

实习细节 程序12.5

// string1.cpp -- String class methods
#include <cstring>                 // string.h for some
#include "string1.h"               // includes <iostream>
using std::cin;
using std::cout;

// initializing static class member

int String::num_strings = 0;

// static method
int String::HowMany()
{
    return num_strings;
}

// class methods
String::String(const char * s)     // construct String from C string
{
    len = std::strlen(s);          // set size
    str = new char[len + 1];       // allot storage
    std::strcpy(str, s);           // initialize pointer
    num_strings++;                 // set object count
}

String::String()                   // default constructor
{
    len = 4;
    str = new char[1];
    str[0] = '\0';                 // default string
    num_strings++;
}

String::String(const String & st)
{
    num_strings++;             // handle static member update
    len = st.len;              // same length
    str = new char [len + 1];  // allot space
    std::strcpy(str, st.str);  // copy string to new location
}

String::~String()                     // necessary destructor
{
    --num_strings;                    // required
    delete [] str;                    // required
}

// overloaded operator methods    

    // assign a String to a String
String & String::operator=(const String & st)
{
    if (this == &st)
        return *this;
    delete [] str;
    len = st.len;
    str = new char[len + 1];
    std::strcpy(str, st.str);
    return *this;
}

    // assign a C string to a String
String & String::operator=(const char * s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}

    // read-write char access for non-const String
char & String::operator[](int i)
{
    return str[i];
}

    // read-only char access for const String
const char & String::operator[](int i) const
{
    return str[i];
}

// overloaded operator friends

bool operator<(const String &st1, const String &st2)
{
    return (std::strcmp(st1.str, st2.str) < 0);
}

bool operator>(const String &st1, const String &st2)
{
    return st2 < st1;
}

bool operator==(const String &st1, const String &st2)
{
    return (std::strcmp(st1.str, st2.str) == 0);
}

    // simple String output
ostream & operator<<(ostream & os, const String & st)
{
    os << st.str;
    return os; 
}

    // quick and dirty String input
istream & operator>>(istream & is, String & st)
{
    char temp[String::CINLIM];
    is.get(temp, String::CINLIM);
    if (is)
        st = temp;
    while (is && is.get() != '\n')
        continue;
    return is; 
}

其中需要注意的是默认构造函数String::String();中使用的是str = new char[1];而不是str = new char;虽然两种分配的内存是一样的,但是前者和析构函数(析构函数中包含语句delete [ ] str;)兼容,后者不兼容。

还记得吗?(在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[ ](包括中括号)来分配内存,则应使用delete[ ](包括中括号)来释放内存。)

delete[ ]和使用new [ ]初始化指针和空指针都兼容。所以默认构造函数中语句

str = new char[1];
str[0] = '\0';  

可替换成:

str = 0;

关于空指针:

在c++98中,0可以用来表示空指针,有些程序员使用(void*)0来标识空指针(但是空指针内部表示可能不是0)还有程序员使用NULL(c语言中表示空指针的宏)。在c++11中,引入关键字nullptr,用于表示空指针,但是依然可以像以前一杨使用0,但是还是建议使用关键字,就像下面这样:

str = nullptr;

下面介绍三个比较成员函数:其中使用了strcmp()函数,该函数接收两个参数,如果第一个参数位于第二个参数之前,则该函数返回一个负值;如果两个字符串相同,则返回0;如果第一个参数位于第二个参数之后,则返回一个正值。另外,将比较函数作为友元,有助于String对象与常规的C字符串进行比较。

函数char & operator[](int i); 使得可以使用中括号表示法来访问其中的字符:通过对[ ]运算符进行重载,在c++中的中括号运算符,一个操作数位于第一个中括号前面,另一个操作数位于中括号之间。所以在表达式city[0]中,city是第一个操作数,[ ]是运算符,0是第二个操作数。

假设opera是一个String对象,对于表达式opera[4],首先c++将查找名称和特征标与此相同的方法:

String :: operator[] (int i);

然后编译器使用下面的函数调用来代替表达式opera[4];

opera.operator[](4)

也就是opera对象调用该方法,数组下标4成为该函数的参数。

静态类成员函数

可以将成员函数声明为静态的(函数声明必须使用关键字static,但是如果函数定义时独立的,则其中不能包含关键字static)。静态数据成员有两个结果:

(1)首先,它不能通过对象调用静态成员函数,甚至不能使用this指针。

(2)如果静态成员函数是在共有部分声明的,则可以使用类名和作用域解析运算符来调用它。例如类声明文件中的函数:static int HowMany();调用它的方式如下:

int const = String::HowMany();

注意,不是类对象调用它,而是类名::

(3)由于静态数据成员函数不与特定的对象相关联,因此只能使用静态数据成员,这里的HowMany()函数可以访问静态成员num_strings,但是不能访问str和len。

(4)可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为,例如,类接口可以控制显示类内容的方法所用的格式。

程序中进一步重载了赋值运算符,可以实现的功能是:可以将常规字符串复制到String对象中。

如果不对赋值运算符进行进一步重载,那么在函数中想要实现这个功能,一般就是像下面这样编写代码:

String name;        
char temp[40];
cin.getline(temp, 40);    //使用getline()读取了一个字符串,并将这个字符串放置到String对象中
name = temp;

最后的语句name = temp;的工作过程是:使用构造函数String(const char *)来创建一个临时的String对象(只有一个参数的构造函数被用作转换函数) ,其中包含temp中的字符串副本(strcpy函数的结果)。然后在使用String&String::operator = (const String &)函数将临时对象赋值到name对象中。最后再调用析构函数~String()删除临时对象。但是这不是一个最好的办法,最好的办法是进一步重载运算符,也就是实现细节中的代码:

String & String::operator=(const char * s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}

首先释放str指向的内存,并为新字符串分配足够的内存。

istream & operator>>(istream & is, String & st)
{
    char temp[String::CINLIM];
    is.get(temp, String::CINLIM);
    if (is)
        st = temp;
    while (is && is.get() != '\n')
        continue;
    return is; 
}

重载运算符>>提供了一个将键盘输入行读入String对象中的简答方法。它设置输入的字符数不多余String::CINLIM,并丢弃多余的字符。在if条件下,在某种条件下(如果到达文件尾或者get(char*),int)读取的是一个空行)导致输入失败,istream对象的值将置为false。

主函数文件(对String类进行测试)程序12.6

// sayings1.cpp -- using expanded String class
// compile with string1.cpp
#include <iostream>
#include "string1.h" 
const int ArSize = 10;
const int MaxLen =81;
int main()
{
    using std::cout;
    using std::cin;
    using std::endl;
    String name;
    cout <<"Hi, what's your name?\n>> ";
    cin >> name;

    cout << name << ", please enter up to " << ArSize
         << " short sayings <empty line to quit>:\n";
    String sayings[ArSize];     // array of objects
    char temp[MaxLen];          // temporary string storage
    int i;
    for (i = 0; i < ArSize; i++)
    {
        cout << i+1 << ": ";
        cin.get(temp, MaxLen);
        while (cin && cin.get() != '\n')
            continue;
        if (!cin || temp[0] == '\0')    // empty line?
            break;              // i not incremented
        else
            sayings[i] = temp;  // overloaded assignment
    }
    int total = i;              // total # of lines read

    if ( total > 0)
    {
        cout << "Here are your sayings:\n";
        for (i = 0; i < total; i++)
            cout << sayings[i][0] << ": " << sayings[i] << endl;

        int shortest = 0;
        int first = 0;
        for (i = 1; i < total; i++)
        {
            if (sayings[i].length() < sayings[shortest].length())
                shortest = i;
            if (sayings[i] < sayings[first])
                first = i;
        }
        cout << "Shortest saying:\n" << sayings[shortest] << endl;;
        cout << "First alphabetically:\n" << sayings[first] << endl;
        cout << "This program used "<< String::HowMany() 
             << " String objects. Bye.\n";
    }
    else
        cout << "No input! Bye.\n";
// keep window open 
/*    if (!cin)
        cin.clear();
    while (cin.get() != '\n')
        continue; */ 
   return 0; 
}

输出结果如下:

Hi, what's your name?
>> yang guo
yang guo, please enter up to 10 short sayings <empty line to quit>:
1: jdoqdwpfwpqjfwe
2: ndoqwnfiqw
3: jdowpef
4: jdpowjfpoqw
5: nwenf
6:
Here are your sayings:
jdoqdwpfwpqjfwe
ndoqwnfiqw
jdowpef
jdpowjfpoqw
nwenf
Shortest saying:
nwenf
First alphabetically:
jdoqdwpfwpqjfwe
My favorite saying:
ndoqwnfiqw
Bye.

在构造函数中使用new时应注意的事项

(1)如果在构造函数中使用new来初始化指针成员,应该在析构函数中使用delete。

(2)new和delete必须互相兼容,new对应于delete,new[ ]对应于delet[ ];

(3)如果有多个析构函数,则必须以相同的方式使用new,要么都带中括号,要么就都不带。因为只有一个析构函数,所有的构造函数都必须与之兼容。但是,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空,这是由于delete和delete[ ]都可以用于空指针。

(4)复制构造函数最好是深度复制,复制构造函数应该分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址,并且更新所有的静态类成员。

(5)应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。

下面有两个错误地实例:

其中,第一个构造函数没有使用new来初始化str,对默认对象调用析构函数时,析构函数必须使用delete来释放str,但是对不是使用new分配的内存使用delete时,结果是不确定的。可以将其修改成下面任意一种形式:

形式1:字符数组

形式2:直接使用0定义空字符

第二个构造函数也有问题:

问题1:虽然使用了new来分配内存,但是分配的内存量不对,语句str = new char;返回的内存块只能保存一个字符,要是视图将过长的字符串保存在其中,会导致内存问题;

问题2:这里使用的new不带中括号。

包含类成员的类的成员复制

例如:

class Magazine
{
private:
    String title;
    string publisher;
    ……
}

这个类中有前面定义的String类变量title,也有标准的string类变量publisher。但是不需要为Magazine单独编写构造函数和赋值运算符,默认的逐成员复制和赋值行为有一定的智能性。假设将一个Magazine赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。也就是说,复制成员title时(a = Magazine b),将使用String的复制构造函数,而将成员title赋给另一个Magazine对象时(A = Magazine.title),将使用String的赋值运算符。还有更加复杂的情况,例如Magazine类因其他成员需要定义复制构造函数和赋值运算符时,就必须显式地调用String和string的复制构造函数和赋值运算符,在后面会介绍。

有关返回对象的说明

当成员函数或者独立函数返回对象时,有几种返回方式可供选择。(1)返回对象的引用(2)指向对象的const引用(3)const对象。下面依次复习它们

(1)返回指向const对象的引用

使用这种类型的返回值一般都是旨在提高效率,例如

 关于这程序需要注意三点:

(1)第一个版本返回的是对象,它需要调用复制构造函数,第二个版本返回的是引用,它不需要调用。所以第二个版本需要做的工作更少,效率高。

(2)引用指向的对象应该在调用函数执行时存在。

(3)v1和v2都被声明为const,所以返回值也必须是const类型,这样才匹配。

 返回指向非const对象的引用

适用场景:重载赋值运算符(旨在提高效率)以及重载与cout一起使用<<运算符(必须这样做)。

场景1:重载赋值运算符operator=()的返回值用于连续赋值:

s2.operator=()的返回值被赋给s3,所以返回String对象或者String对象的引用都是可行的。返回引用可以避免使用复制构造函数来创建一个新的String对象,提高效率。并且返回类型是非const类型,可以对其进行修改。

场景2:重载与cout一起使用<<运算符

其中第二行语句可以转化成operator<<(cout , s1),其返回值是一个用于显示字符串的“is coming”的对象。返回的类型必须是ostream &,而不你能仅仅是ostreeam。因为如果是返回类型是ostream,将要求调用ostream类的复制构造函数,而ostream没有公有的复制构造函数。但是返回引用就不会有任何问题,因为cout已经在调用函数的作用域内。

返回对象

如果被返回的对象是被调用函数中的局部变量,就不应该按引用方式返回它,因为在调用函数执行完毕时,局部变量将调用其析构函数。这个在前面的笔记中也有提及到应该避免返回指向临时变量的引用。当控制权回到调用函数时,引用指向的对象将不再存在,这种情况下就应该返回实际的对象。同样,返回实际对象后,存在调用复制构造函数来出创建被返回对象的开销,也是无法避免的(语句net = force1 + force2)。例如:

返回const对象

适用于避免修改返回值

总之,如果方法或函数要返回局部变量,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来返回的对象。

如果方法或函数要返回一个没有公有复制构造函数的类(如ostream)的对象,它必须返回一个指向这种对象的引用。

有些方法和函数(如重载的赋值运算符)可以返回对象也可以返回指向对象的引用,在这种情况下,应该首选引用,效率更高。

使用指向对象的指针

下面的这个程序改进之前的主函数程序,增加了两个指向String类对象的指针(之前是使用数组索引的),来跟踪最短的字符串(指针shortest)和按字母排在最前面的字符串(指针first)。初次之外,它还是使用指针favorite指向new创建的未被命名的对象。下面将逐个指针介绍,其中favorite是使用new为整个对象分配新内存,开辟了新的内存空间,会特别介绍一下。

程序12.7

// sayings2.cpp -- using pointers to objects
// compile with string1.cpp
#include <iostream>
#include <cstdlib>      // (or stdlib.h) for rand(), srand()
#include <ctime>        // (or time.h) for time()
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
    using namespace std;
    String name;
    cout <<"Hi, what's your name?\n>> ";
    cin >> name;

    cout << name << ", please enter up to " << ArSize
         << " short sayings <empty line to quit>:\n";
    String sayings[ArSize];
    char temp[MaxLen];               // temporary string storage
    int i;
    for (i = 0; i < ArSize; i++)
    {
        cout << i+1 << ": ";
        cin.get(temp, MaxLen);
        while (cin && cin.get() != '\n')
            continue;
        if (!cin || temp[0] == '\0') // empty line?
            break;                   // i not incremented
        else
            sayings[i] = temp;       // overloaded assignment
    }
    int total = i;                   // total # of lines read

    if (total > 0)
    {
        cout << "Here are your sayings:\n";
        for (i = 0; i < total; i++)
            cout << sayings[i] << "\n";

    // use pointers to keep track of shortest, first strings
        String * shortest = &sayings[0]; // initialize to first object
        String * first = &sayings[0];
        for (i = 1; i < total; i++)
        {
            if (sayings[i].length() < shortest->length())
                shortest = &sayings[i];
            if (sayings[i] < *first)     // compare values
                first = &sayings[i];     // assign address
        }
        cout << "Shortest saying:\n" << * shortest << endl;
        cout << "First alphabetically:\n" << * first << endl;

        srand(time(0));
        int choice = rand() % total; // pick index at random
    // use new to create, initialize new String object
        String * favorite = new String(sayings[choice]);
        cout << "My favorite saying:\n" << *favorite << endl;
        delete favorite;
    }
    else
        cout << "Not much to say, eh?\n";
    cout << "Bye.\n";
// keep window open
/*    if (!cin)
        cin.clear();
    while (cin.get() != '\n')
        continue;
    cin.get();
*/
    return 0; 
}

在上述程序中,指针shortest指针最初只是指向第一个对象,当程序找到比它更短的对象时就会把shortest重新设置为指向该对象,同样,指针first也是,所以这两个指针都是指向已有的对象,并不创建新对象,所以这些指针不要求使用new来分配内存。

但语句 

String * favorite = new String(sayings[choice]);

这里指针favorite指向new创建未命名对象,这就意味着使用对象sayings[choice]来初始化新的string对象,就会调用复制构造函数,因为复制构造函数(const String &)的参数类型与初始化(String [choice])匹配。程序12.4、程序12.5和程序12.7中分别从两个层面介绍new的使用,首先,使用new为每一个对象的名称字符串分配存储空间,这是在构造函数中完成的,并且它是字符数组,所以析构函数会使用的是带中括号的delete,这样,当对象被释放时,用于存储字符串内容的内存被自动释放。但是在程序12.7中语句String * favorite = new String(sayings[choice]);使用new为整个对象分配内存,注意是整个对象,而不是为要存储的字符串分配内存,具体而言,也就说为保存字符串地址的str指针(str本身)和len成员分配内存,然后在初始化对象favorite时调用构造函数中new为其字符串分配内存,在将字符串的地址赋给str。所以在程序不再需要favorite时,使用语句 delete favorite;将其释放,但是这个语句只是释放了str指针(str本身)和len成员的内存,释放str指向的字符串内存还是由析构函数完成。

在下述情况中,析构函数将调用:

(1)如果对象时动态变量,则当执行完该对象的程序块时,将调用该对象的析构函数。例如在程序12.3中,执行完毕callme1()函数时,将调用grub的析构函数。

(2)如果对象时静态变量(外部、静态外部或者来自名称空间),则在程序结束时将调用对象的析构函数。程序12.3中的sports就是这样。

(3)如果对象时new创建的,则仅仅当显式使用delete删除对象时,其析构函数才会被调用。

指针和对象小结

 

使用new创建对象图解:

 

定位运算符和类

回顾下定位new的格式:

定位new的使用格式:
1.new(place_address) type;
2.new(place_address) type (initializer_list);
其中place_address必须是一个指针,initializer_list是type类型的初始化列表。

定位运算符可以让程序员在分配内存时能够指定内存位置。下面将在程序12.8中使用常规new和定位new给对象分配内存。其中在构造函数和析构函数中会显示一些消息,可以在输出中观察到对象的历史:

程序12.8

// placenew1.cpp  -- new, placement new, no delete
#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;

class JustTesting
{
private:
    string words;
    int number;
public:
    JustTesting(const string & s = "Just Testing", int n = 0) 
    {words = s; number = n; cout << words << " constructed\n"; }
    ~JustTesting() { cout << words << " destroyed\n";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF];       // get a block of memory

    JustTesting *pc1, *pc2;

    pc1 = new (buffer) JustTesting;      // place object in buffer,定位new,在buffer内存块中放置对象
    pc2 = new JustTesting("Heap1", 20);  // place object on heap,常规new,在堆上放置对象
    
    cout << "Memory block addresses:\n" << "buffer: "
        << (void *) buffer << "    heap: " << pc2 <<endl;
    cout << "Memory contents:\n";
    cout << pc1 << ": ";
    pc1->Show();
    cout << pc2 << ": ";
    pc2->Show();

    JustTesting *pc3, *pc4;
    pc3 = new (buffer) JustTesting("Bad Idea", 6);
    pc4 = new JustTesting("Heap2", 10);
    cout << "Memory contents:\n";
    cout << pc3 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
    
    delete pc2;                          // free Heap1         
    delete pc4;                          // free Heap2
    delete [] buffer;                    // free buffer
    cout << "Done\n";
    // std::cin.get();
    return 0;
}

程序解读:该程序使用new运算符创建了一个512字节的缓冲区(char * buffer = new char[BUF]),然后使用定位new运算符在堆中创建了两个JustTesting对象,也就是下列语句的工作。

pc1 = new (buffer) JustTesting; 
pc3 = new (buffer) JustTesting("Bad Idea", 6);

在使用定位new来创建第二个对象时,这里是pc3指向的,这个新对象会覆盖第一个对象的内存单元。所以类动态的为其成员分配内存,会引发问题。所以为了避免这个问题,程序员需要提供两个位于缓冲区的不同地址,确保这两个内存地址不重叠。就像下面这样:

pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof(JustTesting) ) JustTesting("Better Idea", 6);

其次pc2和pc4都是使用常规new为对象分配内存,将delete用于pc2和pc4时,就会自动为pc2和pc4指向地对象调用析构函数。但是将delete[ ]用于buffer时,不会为使用定位new创建的对象调用析构函数(这里是pc1和pc3指向的)。想释放在buffer中的对象不能像常规new分配的那样,直接使用delete pc2和delete pc4那样,原因是delete可以与常规new运算符配合使用,但是不能与定位new配合使用。其中buffer使用常规new分配的,所以可以使用delete [ ]来释放内存。另外语句delete pc1释放的也是buffer的内存,不是pc1(这里我的理解是buffer和pc1指向的都是内存块中第一个单元)。

在该程序中语句delete [ ] buffer;确实是释放使用常规new运算符分配的整个内存块,但是它没有为定位new运算符在该内存块中创建的对象调用析构函数。(而使用常规new创建的pc2和pc4显式的使用了delete来释放它),解决办法就是显式地(一般情况下是自动调用的,定位new是少数需要显示调用的情形之一)为定位new运算符创建的对象调用析构函数。格式如下,必须指定要销毁的对象。

pc3 -> ~JustTesting();//销毁pc3指向的对象
pc1 -> ~JustTesting();

这里需要注意删除的顺序,先删除了pc3,再删除了pc1,对于使用定位new运算符创建的对象,应以与创建对象相反的顺序进行删除。原因在于晚创建的对象可能会依赖于早创建的对象。最后一点需要注意的是只有所有对象都被销毁时,才能释放用于存储这些对象的缓冲区。下面将在程序12.9中改正这些缺陷:

程序12.9

// placenew2.cpp  -- new, placement new, no delete
#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;

class JustTesting
{
private:
    string words;
    int number;
public:
    JustTesting(const string & s = "Just Testing", int n = 0) 
    {words = s; number = n; cout << words << " constructed\n"; }
    ~JustTesting() { cout << words << " destroyed\n";}
    void Show() const { cout << words << ", " << number << endl;}
};
int main()
{
    char * buffer = new char[BUF];       // get a block of memory

    JustTesting *pc1, *pc2;

    pc1 = new (buffer) JustTesting;      // place object in buffer
    pc2 = new JustTesting("Heap1", 20);  // place object on heap
    
    cout << "Memory block addresses:\n" << "buffer: "
        << (void *) buffer << "    heap: " << pc2 <<endl;
    cout << "Memory contents:\n";
    cout << pc1 << ": ";
    pc1->Show();
    cout << pc2 << ": ";
    pc2->Show();

    JustTesting *pc3, *pc4;
// fix placement new location
    pc3 = new (buffer + sizeof (JustTesting))
                JustTesting("Better Idea", 6);
    pc4 = new JustTesting("Heap2", 10);
    
    cout << "Memory contents:\n";
    cout << pc3 << ": ";
    pc3->Show();
    cout << pc4 << ": ";
    pc4->Show();
    
    delete pc2;           // free Heap1         
    delete pc4;           // free Heap2
// explicitly destroy placement new objects
    pc3->~JustTesting();  // destroy object pointed to by pc3
    pc1->~JustTesting();  // destroy object pointed to by pc1
    delete [] buffer;     // free buffer
    // std::cin.get();
    return 0;
}

输出结果为下:

Just Testing constructed
Heap1 constructed
Memory block addresses:
buffer: 000001D2A3C66680    heap: 000001D2A3C5B110
Memory contents:
000001D2A3C66680: Just Testing, 0
000001D2A3C5B110: Heap1, 20
Better Idea constructed
Heap2 constructed
Memory contents:
000001D2A3C666B0: Better Idea, 6
000001D2A3C67A30: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Better Idea destroyed
Just Testing destroyed

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值