类和动态内存分配
一、静态类成员
- 静态类成员特点:无论创建了多少个对象,程序都只创建一个静态类变量的副本。所有对象共同使用。
- 初始化静态成员变量:
int StringBad::num_strings=0;
注意事项:
-
不能再类声明中初始化静态成员变量,对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,因为静态类成员单独存储的,而不是对象的组成部分。注意,初始化语句指出了类型,并使用了作用域,但没有使用关键字static。
-
初始化在方法文件中,而不是在类声明文件中进行的。对于不能在类声明中初始化静态数据成员的一种情况,静态数据成员为const整数类型或枚举类型。
-
静态数据成员在类声明中声明,在包含类方法·的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是const整数类型或枚举类型,则可以在类声明中初始化。
#include <iostream>
#include<cstring>
using namespace std;
class StringBad
{
private:
char *str;
int len;
static int num_strings;
public:
StringBad(const char * s);//构造函数
StringBad();//默认构造函数
~StringBad();//析构函数
friend ostream &operator <<(ostream &os,const StringBad &st);
};
int StringBad::num_strings=0;
StringBad::StringBad(const char *s)
{
len=strlen(s);//设置大小
str=new char[len+1];//分配存储
strcpy(str,s);//初始化指针
num_strings++;//设置对象数目
cout<<num_strings<<":\""<<str
<<"\"object created\n";
}
StringBad::StringBad()//这样不能准确地记录对象个数,解决办法提供显示的复制构造函数StringBad::StringBad(cosnt String & s){num_strings++}
{
len=4;
str=new char[4];
strcpy(str,"C++");//默认字符串
num_strings++;
cout<<num_strings<<":\""<<str
<<"\"default object created\n";
}
StringBad::~StringBad()
{
cout<<"\""<<str<<"\"object deleted,";
--num_strings;
cout<<num_strings<<" left\n";
delete []str;
}
ostream &operator <<(ostream &os,const StringBad &st)
{
os<<st.str;
return os;
}
void callme1(StringBad &rsb);
void callme2(StringBad &sb);
int main()
{
cout<<"String an inner block.\n";
StringBad ob1("wwwww");
StringBad ob2("qqqqq");
StringBad ob3("ddddd");
cout<<"ob1:"<<ob1<<endl;
cout<<"ob2:"<<ob2<<endl;
cout<<"ob3"<<ob3<<endl;
callme1(ob1);//将ob1传递callme1()函数,并显示
cout<<"ob:"<<ob1<<endl;
callme2(ob2);//按值传递(而不是引用)传递ob2,结果有严重问题,将ob2作为参数来传递导致析构函数调用,按值传递可用防止原始参数被修改,但原始字符串无法识别,导致显示别的字符
cout<<"ob2:"<<ob2<<endl;
StringBad pt=ob3;
cout<<"pt:"<<pt<<endl;
StringBad os;
os=ob1;
cout<<"os:"<<os<<endl;
return 0;
}
void callme1(StringBad &rsb)
{
cout<<"String pass by reference:\n";
cout<<"\""<<rsb<<"\"\n";
}
void callme2(StringBad &sb)
{
cout<<"String pass by value:\n";
cout<<"\""<<sb<<"\"\n";
}
StringBad::StringBad(const char *s)
{
len=strlen(s);//设置大小
str=new char[len+1];//分配存储,str是个指针,因此构造函数必须提供内存存储字符串。初始化对象时,可以给构造函数传递一个字符串指针。
strcpy(str,s);//初始化指针
num_strings++;//设置对象数目
cout<<num_strings<<":\""<<str
<<"\"object created\n";
}
-
使用strlen()函数计算字符串的长度,并对len成员进行初始化。接着,使用new()分配足够的空间来保存字符串,然后将新内存的地址赋给str成员。(strlen()返回字符串长度,但不包含末尾的空字符,因此构造函数将len加1,使分配的内存能够存储包含空字符的字符串)
-
构造函数使用strcoy()将传递的字符串复制到内存中,并更新对象计数。
-
要理解这种方法,必须知道字符串并不保存在对象中,字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串信息。
-
在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存,使用new[]来分配内存,则应使用delete[]来释放内存。
不能这样做:
str=s;//这只保存了地址,而没有创建字符串副本
二、特殊成员函数
1.概述
c++提供了下面这些成员函数
- 默认构造函数,没有定义构造函数。
- 默认析构函数,没有定义。
- 复制构造函数,没有定义。
- 赋值运算符,没有定义。
- 地址运算符,没有定义。
- 隐式地址运算符返回调用对象的地址(即this指针的值)。
2.默认构造函数
没有提供任何构造函数,将创建默认构造函数。例如:定义一个Klunk类,但没有提供任何构造函数,则编译器提供默认构造函数:
Klunk::Klunk(){}//不接受任何参数,不执行任何操作 ,因为创建对象时会调用
定义了构造函数,将不会提供默认构造函数,如果创建对象时不显示地对它进行初始化,则必须显示定义默认构造函数。没有任何值,但可以使用它来设置特定的值:
Klunk::Klunk()
{
klunk_ct=0;
...
}
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。例如:Klunk类可以包含下述内联构造函数:
Klunk(int n=0)
{Klunk_ct=n;}
只能 有一个默认构造函数。
3.复制构造函数(拷贝构造函数)
复制构造函数用于将一个对象复制到新对象中。它用于初始化过程中(包括按值传递函数),而不是常规的赋值过程中,类型如下:
Class_name(const Class_name &);
它接受一个指向类对象的常量引用作为参数。对于复制构造函数,需要知道两点:何时调用和有和功能。
创建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。最常见的情况是将新对象显示地初始化为现有的对象。例如:假设motto是一个StringBad对象,则下面的四种声明都将调用赋值构造函数:
StringBad ditto(motto);
StringBad metoo=motto;
StringBad also=StringBad(motto);
StringBad * pStringBad = new StringBad(motto);//StringBad (cosnt StringBad &)
上面的StringBad类中callme2(ob2)调用复制构造函数。程序使用复制构造函数初始化sb->callme2()
函数的。StringBad型形参。由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。可节省调用构造函数的时间和空间。
4.默认构造函数功能
默认的复制构造函数逐个复制非静态成员(成员复制也称浅复制),复制的是成员的值。
在StringBad类中:
StringBad pt =ob3;
与下面的代码等效(只是由于私有成员是无法访问的,因此这些代码不能通过编译):
StringBad pt;
pt.str=ob3.str;
pt.len=ob3.len;
如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。静态成员不受影响。因为属于整个类,而不是对象。下面说明了隐式复制构造函数执行的操作:
提示:如果类中包含静态数据成员,其值将在新对象被创建时发生变化,则应该提供一个显式复制构造函数来处理静态数据成员的值变化问题。
StringBad::StringBad(cosnt String & s){num_strings++}
如果类中包含使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深拷贝,解决浅拷贝同·以堆区被释放多次。
StringBad::StringBad(const StringBad & st)
{
num_strings++;
len=st.len;
str=new char[len+1];
strcpy(str,st.str);
cout<<num_strings<<":\""<<str
<<"\"object created\n";
}
5.赋值运算符
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。这种运算符的原型如下:
Class_name & Class_name:: operator=(const Class_name &);
它接受并返回一个指向类对象的引用。例如·:StringBad类的赋值运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &);
赋值运算符的功能以及何时使用它,将已有的对象赋给另一个对象时,将使用重载的赋值运算符;
StringBad ob1("wwww");
...
StringBad per;
per=ob1;
对于由于默认赋值运算符不合适而导致的问题(常见情况是类成员为使用new初始化的指针),解决办法是提供赋值运算符(进行深度复制)定义。其实现与复制构造函数相似,但也有区别:
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete来释放这些数据。
- 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用。
三、c++11空指针
在老式中,字面值0有两个含义:1.表示数字值为0;2.空指针。还有使用NULL表示空指针,但是c++11引入了关键字nullptr,表示空指针。
四、比较成员函数
-
在String类中,执行比较函数操作的方法有3个,如果按字母顺序,第一个字符串在第二个字符串之前,则
operator<()
函数返回true。 -
要实现字符串比较函数,最简单的方法是使用标准的
trcmp()
函数。 -
如果依照字母顺序,第一个参数位于第二个参数之前,则该函数返回一个负值;
-
如果两个字符串相同,则返回0;如果第一个参数位于第二个参数之后,则返回一个正值。
bool operator<(const String &st1,const String &st2)
{
if(strcmp(st1.str,st2.str)<0)
return true;
else
return false;
}
因为内置的>
运算符返回的一个布尔值,所以将代码进一步简化为:
bool operator<(const String &st1,const String &st2)
{
return (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 (strcmp(st1.str,st2.str) == 0);
}
第一个定义利用了<
运算符来表示>
运算符,对于内联函数,这是一种很好的选择。
将比较函数作为友元,有助于将String对象与常规的C字符串进行比较。例如,假设answer是String对象,则下面的代码:
if("love"b== answer)
将被转换为:
if(operator==("love, answer))
然后,编译器将使用某个构造函数将代码转换为:
if(operator == (String("love"),answer))
这与原型是匹配的。
五、使用中括号表示法访问字符
对于标准c-风格字符串来说,可以使用中括号来访问其中的字符:
char city[40]="Amsterdam";
cout<<city[0]<<endl;//显示A
- 在c++中,两个中括号组成一个运算符
-
中括号运算符,可以使用方法operator[]()
来重载该运算符。 - 。通常,二元运算符(带两个操作数)位于两个操作数之间,例如
2+5
但对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。 - 因此,在表达式
city[0]
中,city是第一个操作数,[]
是运算符,0是第二个操作数。
假设opera是String对象:
String opera("The Magic Flute");
//则对于表达式opera[4],c++将查找名称和特征标与此相同的方法:
String::operator[](int i)
//如果找到匹配的原型,编译器将使用下面的函数调用来替代表达式opera[4]:
opera.operator[](4)
//opera对象调用该方法,数组下标4成为该函数的参数。
下面是该方法的简单实现:
char & String::operator[](int i)
{
return str[i];
}
// 这时语句:cout<<opera[4];
//被转换为:
cout<<opera.operator[](4);//返回值是opera.str[4](M)由此,公有方法可以访问私有数据
//给返回类型声明为char&,便可以给特定元素赋值。例如:
String means("might");
means[0]='r';//私有数据,但由于operator[]()是类的一个方法,因此可以修改。
//若下面的常量是对象:
const String answer("futile");
//定义了operator[](),则下面代码将错误:
cout<<answer[1];
//因为answer是常量,而上述方法无法确保不修改数据。
//但在重载时,c++将区分常量和非常量的特征标,因此可以提供另一个仅供const String对象使用的operator[]()版本:
const char & String::operator[](int i) const
{
return str[i];
}
//定义了上面之后,就可以读写常规String对象了;而对于const String对象,则只能读取其数据:
String text("Once upon a time");
const String answer ("futile");
cout<<text[1];//ok
cout<<answer[1];//ok
cin>>text[1];//ok
cin>>answer[1];//error
六、静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义时独立的则其中不能包含关键字static),这样做会有两个重要的后果:
- 不能通过对象调用静态成员函数。实际,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的。则可以使用类名和作用域解析运算符来调用它。例如:可以给String类添加一个名为HowMany()的静态成员函数,方法是在类声明中添加:
static int HowMany(){return num_strings;}
//调用方式:
int count=String::HowMany();
-
静态成员函数不与特定的对象关联,因此只能使用静态数据成员。例如:静态方法
HowMany()
可以访问静态成员num_string
,但不能访问别的str,len
。 -
可以使用静态成员函数设置类级标记,控制某些类接口的行为。例如:类级标记可以控制显示类内容的方法所使用的格式。
七、进一步重载赋值运算符
假设要将常规字符串复制到String对象中。例如,假设使用getline()读取一个字符串,并要将这个字符串放置到String对象中,前面定义的类方法让你能够这样写代码:
String name;
char temp[40];
cin.getline(temp,40);
name = temp;//使用构造函数转换类型
如果经常这样做,不是理想方案。回顾最后一条语句如何工作。
- 程序使用构造函数String(const char *)来创造临时String对象,其中包含temp中的字符串副本。只有一个参数的构造函数被用作转换函数。
- 下面程序使用String & String::operator=(const String &)函数临时对象中的信息复制到name中。
- 程序调用析构函数~String()删除临时对象。
头文件:
#ifndef STRING2_H
#define STRING2_H
#include <iostream>
using std::ostream;
using std::istream;
class String
{
private:
char * str;//字符串的指针
int len;//字符串的长度
static int mum_strings;
static const int CINLIM =80;//cin输入限制
public:
//构造函数和其他方法
String(const char * s);//构造函数
String();//默认构造函数
String(const String &);//复制构造函数
~String();//析构函数·
int length()const{return len;}
//重载运算符方法
String & operator =(const String &);
String & operator =(const char *);
char & operator [](int i);
const char & operator [] (int i)const;
//重载友元运算符
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);
friend ostream & operator <<(ostream & os,const String &st);
friend istream & operator >>(istream & is,String & st);
//静态函数
static int HowMany();
};
#endif // STRING2_H
源文件1:
#include <cstring>
#include "string2.h"
using namespace std;
//初始化静态类成员
int String::num_strings=0;
//静态方法
int String::HowMany()
{
return num_strings;
}
//类方法
String::String(const char *s)//从C字符串构造字符串
{
len=strlen(s);//设置大小
str = new char[len+1];//分配存储空间
strcpy(str,s);//初始化指针
num_strings++;//设置对象计数
}
String::String()//默认构造函数
{
len = 4;
str = new char[1];
str[0] = '\0';//默认字符串
num_strings++;
}
String::String(const String &st)
{
num_strings++;//处理静态成员更新
len = st.len;//相同长度
str = new char [len+1];//分配空间
strcpy(str,st.str);//将字符串复制到新位置
}
String::~String()//必要的析构函数
{
--num_strings;
delete [] str;
}
//将字符串复制给字符串
String & String::operator =(const String & st)
{
if(this == &st)
return *this;
delete [] str;
len = st.len;
str = new char[len+1];
strcpy(str,st.str);
return *this;
}
//将C字符串复制给字符串
String & String::operator =(const char * s)//重载赋值运算符,使之能够直接使用常规字符串,这样不用创建和删除临时对象,一般来说,必须释放str指向的内存,并为新字符串分配足够的内存。
{
delete [] str;
len = strlen(s);
str = new char[len+1];
strcpy(str,s);
return *this;
}
//非常量字符串的读写字符访问
char & String::operator [](int i)
{
return str[i];
}
//常量字符串的只读字符访问
const char & String::operator [](int i)const
{
return str[i];
}
bool operator <(const String &st1,const String &st2)
{
return (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 (strcmp(st1.str,st2.str)==0);
}
//简单字符串输出
ostream & operator <<(ostream & os ,const String &st)
{
os << st.str;
return os;
}
//字符串输入
istream & operator >>(istream & is,String & st)//提供一种将键盘输入行读入到String对象中的方法
{
char temp[String::CINLIM];
if(is)
st=temp;
while (is && is.get() !='\n') {
continue;
return is;
}
}
源文件2:
#include <iostream>
#include "string2.h"
using namespace std;
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
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];//临时字符串存储
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')
break;
else
sayings[i] = temp;
}
int total = i;
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 object .Bye.\n";
}
else
cout<<"No input! Bye.\n";
return 0;
}
}
运行:
八、在构造函数中使用new时应该注意的事项
用new初始化对象的指针成员时必须特别小心。所以我们应该这样做:
-
如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete。
-
new和delete必须相互兼容,new对应于delete,new[]对应于delete[]。
-
多个构造函数,必须使用相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的析构函数都必须与他兼容。然而,可以在一个析构函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或nullptr),这是因为delete可以用于空指针。
-
应定义一个·赋值运算符,通过深度复制一个对象复制给另一个对象。通常,该方法与下面类似:
String & string::operator=(const String & st)
{
if(this==&st)
return *this;
delete [] str;
len=st.len;
str=new char [len+1];//获取新字符串空间
strcpy(str,st.str);//复制字符串
return *this;
应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面的相似。
String::String(const String &st)
{
num_string++;
len=st.len;
str=new char [len+1];
strcpy(str,st.str);
}
具体的说,复制构造函数应分配足够的空间来储存复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
九、应该和不应该
下面是不正确的示例(指出什么是不应该当做的)以及应该良好的构造函数示例:
String::String()
{
str="default string";//no new[]
len=strlen(str);
}
String::String(const char * s)//使用new但是分配的内存不正确,只能保存一个字符。另外使用的new不带中括号
{
len=strlen(s);
str=new char;//no []
strcpy(str,s);//np room
}
String::String(const Sttring & st)
{
len=st.len;
str=new char[len+1];//good,成功分配空间
strcpy(str,st.str);//good,成功复制
}
注释:第一个构造函数没有使用new来初始化str,对默认对象调用析构函数时,析构函数使用delete来释放str,对不是使用new初始化的指针使用delete时,结果将不确定,并可能有害,所有该构造函数修改为下面的任何一种形式:
String::String()
{
len=0;
str=new char[1];
str[0]='\0';
}
String::String()
{
len=0;
str=0;
}
String::String()
{
static cosnt char * s= "c++";//初始化一次
len=strlen(s);
str=new char[len+1];
strcpy(str,s);
最后,上面正确的第三个应该与下面协调工作:
String::~String()
{
delete [] str;
}
十、包含类成员的类的逐成员复制
假设类成员的类型为String类或标准string类:
class Magazine
{
private:
String title;
string publisher;
...
}
- 若将Magazine对象复制或赋值给另一个Magazine对象,逐成员复制将使用成员类型定义的复制构造函数和赋值运算符。
- 复制成员title时,将使用String的复制构造函数,而将成员title赋给另一个Magazine对象时,将使用String的赋值运算符。
- 其他成员需要定义复制构造函数和赋值运算符,情况复杂;
- 这些函数将显示的调用String和string的复制构造函数和赋值运算符。
十一、 复习重载<<运算符
要重新定义<<运算符,以便将它和cout一起用来显示对象的内容,请定义下面的友元运算符函数:
ostream & operator<<(ostream & os,const c_name & obj)
{
os<<...;//显示对象内容
return os;
}
注释:其中c_name是类名。如果该类提供了能够返回所需内容的公有方法,则可在运算符函数中使用这些方法,这样便不用将它们设置为友元函数了。
十二、复习转换函数
要将单个值转换为类类型,需要创建原型如下所示的类构造函数:
c_name(type_name,value);//c_name为类名,type_name是要转换的类型的名称。
要将类转换为其他类型,需要创建原型如下所示的类成员函数:
operator type_name();
虽然该函数没有声明返回类型,但应该返回所需类型的值。
使用转换函数时要小心。可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。
十三、其构造函数使用new的类
如果类使用new运算符来分配类成员指向的内存,在设计时应采取一些预防知识:
- 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete释放分配的内存。
- 析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
- 使用new,应该用delete释放分配内存;使用new[],应该使用delete[]释放分配内存。
- 应定义一个分配内存的复制构造函数,这样程序将能够将类对象初始化为另一个类对象。格式:
className(const className &)
- 应定义一个重载赋值运算符的类成员函数,其函数定义如下(c_pointer是c_name的类成员,类型为指向type_name的指针)。下面使用new[]来初始化变量c_pointer:
c_name & c_name::operator=(const c_name & cn)
{
if(this==&cn)
return *this;//如果自我分配,则完成
delete [] c_pointer;
//设置复制的类型单位的大小数
c_pointer = new type_name[size];
//然后将数据·复制到c_pointer;
//指向c_pointer位置
...
return *this;
}