通常,最好是在程序运行时(而不是编译时)确定诸如使用多少内存等问题。对于在对象中保存姓名来说,通常C++的做法是,在类构造函数使用new运算符在程序运行时分配所需的内存。
C++分配内存时采取的部分策略与此相同,让程序在运行时决定内存分配,而不是在编译时决定。
// stringbad.h
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_Hclass StringBad
{
private:char* str;int len;static int num_strings;
public:StringBad(const char* s);StringBad();~StringBad();
// friend functionfriend std::ostreaming& operator <<(std::ostreaming os, const StringBad& st);
};#endif
对于此声明,需要注意两点。首先,它使用char*指针(而不是char数组)来表示姓名。意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串的长度。其次,将num_strings成员声明为静态存储类。静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。这对于所有类对象都具有相同值的类私有数据是非常方便的。
// string.cpp -- StringBad class methods
#include <cstring>
#include "stringbad.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);str = new char[len + 1]; // 类成员str是一个指针,因此构造函数必须提供内存来存储str。std::strcpy(str, s); // 若使用str = s,则只是将s地址赋给了str,而并没有保存s指向的内容。num_strings += 1;
}StringBad::~StringBad()
{--num_strings;delete[] str; // 构造函数使用了new char[]。
}
请注意,不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。请注意,初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static。初始化是在方法文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包含在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
注意,静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态类成员是const整数类型或枚举类型,则可以在类声明中初始化。
当StringBad对象过期时,str指针也将过期。但str指向的内存仍被分配,除非使用delete将其释放。删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。
在构造函数中使用new来分配的内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[](包括中括号)来分配内存,则应使用delete[]来释放内存。
StringBad类的问题是由特殊成员函数引起的。具体地说,C++自动提供了下面这些成员函数:
1. 默认构造函数, 如果没有定义构造函数;
2. 默认析构函数, 如果没有定义;
3. 复制构造函数, 如果没有定义;
4. 复制运算符, 如果没有定义;
5. 地址运算符, 如果没有定义;
StringBad类中的问题是由隐式复制构造函数和隐式赋值运算符引起的。
隐式地址运算符返回调用对象的地址(即this指针的值)。
C++11 还提供了另外两个特殊成员函数:移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。
1. 默认构造函数
如果没有提供任何构造函数,C++将创建默认构造函数。也就是说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数例如:
Klunk::Klunk(){}; // implicit default constructor
如果定义了构造函数,C++将不会定义默认构造函数。如果希望在创建对象时不显示地对它进行初始化,则必须显式地定义默认构造函数。这种构造函数没有任何参数,但可以使用它来设置特定的值:
Klunk::Klunk()
{klunt_ct = 0;...
}
带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值。
Klunk(int n = 0){ klunk_ct = n; }
但是默认构造函数只有有一个,不然引起二义性。
2. 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数通常如下;
Class_name(const Class_name& );String::String(const String& st)
{num_strings++;len = st.len;str = new char[len + 1];std::strcpy(str, st.str);
}
对于复制构造函数,需要知道两点:何时调用和有何功能
新建一个对象并将其初始化同类现有对象时,复制构造函数都将被调用。最常见的情况时将新对象显式地初始化为现有的对象。
StringBad motto = {...};// 以下4种声明都调用复制构造函数
StringBad ditto1(motto);
StringBad ditto2 = motto;
StringBad ditto3 = StringBad(motto);
StringBad* ditto4 = new StringBad(motto);
中间两种声明可能会导致使用复制构造函数直接创建ditto2和ditto3,也可以使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给ditto2和ditto3,这取决于具体实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给ditto4指针。
每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回临时对象时,都将使用复制构造函数。记住,按值传递意味着创建原始变量的一个副本。
4. 默认的复制构造函数功能
默认的复制构造函数逐个复制非静态成员(成员复制也叫浅复制),复制的是成员的值。静态成员是属于整个类的,而不是各个对象。
StringBad sports = {...};
StringBad sailor = sports;
一个问题是静态成员变量计数更新问题。如果类中包含这样的静态成员,即其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理静态成员更新问题。
另一个问题隐式构造函数是逐非静态成员复制。
当释放sports后,则sailor.str指向的内存也被释放了,这将导致不确定的,甚至有害的后果。因为试图释放内存两次可能导致程序异常终止。
解决类设计中这种问题的方法是进行深度复制(deep copy)。复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。
必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。
如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针。这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。
Class_name& Class_name::operator =(const Class_name&);
1.赋值运算符的功能以及何时使用它
将已有的对象赋给另一个对象时,将使用重载的赋值运算符:
StringBad headline(...);
StringBad knot;
knot = headline; // 赋值运算符StringBad knot1 = headline; // 复制构造函数
初始化对象时,并不一定调用赋值运算符。knot1初始化的实现可能分两步来实现:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。
与复制构造函数类似,赋值运算符的隐式实现也对成员进行逐个复制。
2. 赋值问题的出在哪里
问题与隐式复制构造函数相同:数据受损。
3. 解决赋值的问题
解决办法是提供赋值运算符(进行深度复制)定义,其实现与复制构造函数类似。但也有一些区别:
由于目标对象可能引用了以前分配的数据,所以函数应该先使用delete来释放这些数据;
函数应当避免将对象赋给自身;否则,给对象重新赋值,释放内存操作可能删除对象的内容。
函数应当返回一个指向调用对象的引用。
通过返回一个对象,函数可以像常规赋值操作那样,进行连续的赋值。
StringBad& StringBad::operator =(const StringBad& st)
{if (this == &st)return *this;delete[] str;len = str.len;str = new char[len + 1];std::strcpy(str, st.str);return *this;
}
如果不是自我赋值,在不首先使用delete运算符,则上述字符串将保留在内存中。由于程序中不在包含该字符串的指针,因此这些内存将被浪费掉。
String::String()
{len = 0;str = new char[1];str[0] = '