例子1:创建一个StringBad类,成员有指向char数组的指针和字符串长度int,以及一个存储字符串数的静态变量。
stringbad.h
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
#include<iostream>
class StringBad{
private:
char * str;
int len;
static int num_str;
public:
StringBad();
StringBad(const char * c);
~StringBad();
friend std::ostream& operator<<(std::ostream& os,const StringBad& s);
};
#endif
1.cpp
#include"stringbad.h"
#include<cstring>
using std::cout;
int StringBad::num_str =0;
StringBad::StringBad(){
str = new char[4];
len =3;
strcpy(str,"C++");
num_str++;
cout<<num_str<<": \""<<str<<"\" default object created\n";
}
StringBad::StringBad(const char* c)
{
len = strlen(c);
str = new char[len+1];
strcpy(str,c);
num_str++;
cout<<num_str<<": \""<<str<<"\" default object created\n";
}
StringBad::~StringBad(){
cout<<"\""<<str<<"\" deleted, ";
num_str--;
delete [] str;
cout<<num_str<<"left\n";
}
std::ostream& operator<<(std::ostream& os,const StringBad& s)
{
os<<s.str;
return os;
}
程序解析:
- 构造函数以及default构造函数用new来为字符串指针分配大小为len+1的动态内存(因为strlen()得到的是字符串的长度,not include '\0')
- 然后用strcpy()来赋值,将字符串复制到新的内存中,字符串并不保存在对象中,而是保存到heap中,对象仅仅是指出该地址在哪。
- 直接用等号的话是不可以的,因为这是指针,str = s只保存了地址,没有创建字符串副本。
- 不能在类声明中初始化静态变量,因为类声明是描述如何分配内存,而不分配内存。对于静态类成员,可以在类声明之外使用单独语句初始化,因为静态类成员是单独存储的,不在哪个对象里,是共享的。如果在头文件初始化,且在多个文件里引用头文件,将多次初始化,发生错误。但静态数据成员的const整数类和枚举类是例外,可以类声明初始化。
- 析构函数里必须包括delete [] str,以及num_str--;
StringBad过期时,str指针也将过期,但str指向的内存仍然被分配。所以必须用delete释放该内存。
2.cpp(将发生很多错误)
#include"stringbad.h"
using std::cout;
void func1(StringBad& s);
void func2(StringBad s);
int main()
{
using std::endl;
{
StringBad s1("I am Jeff11111");
StringBad s2("I am Jeff22222");
StringBad s3("I am Jeff33333");
cout<<"s1 : "<<s1<<endl;
cout<<"s2 : "<<s2<<endl;
cout<<"s3 : "<<s3<<endl;
func1(s1);
cout<<"s1: "<<s1<<endl;
func2(s2);
cout<<"s2: "<<s2<<endl;
cout<<"\n 初始化一个新的对象\n";
StringBad s4 = s3;
cout<<"s4 : "<<s4<<endl;
cout<<"s3 : "<<s3<<endl;
cout<<"\n 赋值一个对象到一个新对象\n";
StringBad s5;
cout<<"赋值之前 s5 : "<<s5<<endl;
s5 = s1;
cout<<"赋值之后 s5 : "<<s5<<endl;
cout<<"此时s2 : "<<s2<<endl;
cout<<"离开代码块: \n";
}
cout<<"Over!\n" ;
return 0;
}
程序解析:
- 按值传递的函数调用之后,接着就是析构函数的调用。
- s2指向的字符串被篡改
- 初始化一个新对象没有构造函数里要打印的信息。
- s2指向的字符串内容变成C++
- 离开代码块之后,应该会调用5次析构函数,但仅仅调用了3次,且第三个既s3还出现乱码,连num_str都没了。
所以问题出现在:
1.用对象来初始化一个新对象没有使用构造函数
2.按值传递函数,将调用析构函数
3.s2所指向的字符串内存地址,被删除了,然后被s5默认构造函数创建的那个"C++"占领了
4.赋值s5=s1并没有把s2的"c++"改掉。
特殊成员函数
当没有相关设计时,C++将提供下面这个成员函数:
1.默认构造函数
5.默认析构函数
2.复制构造函数
3.赋值运算符
4.地址运算符
复制构造函数
- 复制构造函数用于将一个对象复制到新创建的对象中,用于初始化过程中,而不是常规赋值过程中。
原型class_name(const Class-name& )- 何时被调用?
新建一个对象并将其初始化为同类现有对象时,将会出现。比如:StringBad ditto(motto) StringBad metto = motto; StringBad also = StringBad(motto); StringBad* ptr = new StringBad(motto);
其中中间两种情况,可能是直接用复制构造函数创建新对象,或者是复制构造函数创建一个motto对象副本,然后赋值给新对象。最后一种声明使用motto初始化一个匿名对象,然后将新对象地址传递给ptr
- 按值传递函数意味着创建原始变量的一个副本,编译器生成临时变量,使用复制构造函数。然后结束函数时,将销毁副本。就调用了析构函数,由于副本是复制原始变量的值,所以原始变量指针指向的地址就同副本指针指向地址一致,所以改地址会被销毁,而且num_str也会被减一。
- default复制构造函数的功能
就是逐个复制非静态成员(因为静态成员不是对象里的成员,而是整个类的成员)- 解决方法:
1.避免使用这种方法
2.重新定义一个显式复制构造函数
赋值运算符
- 原型如下:
class_name& class_name::operator=(const class_name &)- 何时使用?
将已有的对象赋值给另一个对象时,将使用。但初始化时,一般都是自动使用复制构造函数。- 赋值运算符也是将非静态成员逐个赋值,然后初始化s5的时候,s2指针所指向的内存地址其实已经被delete了,所以是个空出来的位置。所以s5初始化就指向这个位置(原来是s2的指针所指),后来赋值s5 = s1,s5的指针就指向s1指针所指的地方。所以"C++"的地址的内容并没有被改变。也是成员复制问题。
修改地方如下:
复制构造函数
StringBad::StringBad(const StringBad& s)
{
len = s.len;
str = new char[len+1];
strcpy(str,s.str);
num_str++;
cout<<num_str<<": \""<<str<<"\" default object created\n";
}
赋值运算符
StringBad& StringBad::operator=(const StringBad& s)
{
if(this == &s)
return *this;
delete [] str;
len = s.len;
str = new char[len+1];
strcpy(str,s.str);
return *this;
}
程序解析:
- 复制构造函数
1.它的参数是类的引用(如果是按值还要创建副本效率低,而且类的引用,主程序按值传递也适用)- 赋值运算符
1.首先返回的是类的引用(因为s5=s1,懂了吧)
2.然后判断该地址是不是本身(用隐式地址==&s)
3.不是的话要先删除原本的地址,然后再重新分配。
(下一节内容....)
这一节要往类里面加入的功能:
int length() const {return len} friend bool operator<(const StringBad& s1,const StringBad& s2); friend bool operator==(const StringBad& s1,const StringBad& s2); friend bool operator>(const StringBad& s1,const StringBad& s2); friend istream& std::operator>>(std::istream& is, StringBad& s); StringBad& operator=(const char * s); char& operator[](int n); const char& operator[](int n) const; static int Howmany();
修改后的默认函数:
StringBad::StringBad(){
len = 0;
str = new char[1] ;
str[0] = nullptr ;
num_str++;
}
1.用str = new char[1]而不用new char是因为要对照析构函数里的delete
2.用nullptr来表示空指针(仅支持C+11版本,可以NULL或0)
比较函数
可以用strcmp(S1,S2)来返回bool值。当第一个参数小于第二个参数时,返回的是一个负值。等于时返回0。大于时返回一个正值。
bool operator<(const StringBad& s1,const StringBad& s2)
{
return (std::strcmp(s1.str,s2.str)<0);
}
bool operator>(const StringBad& s1,const StringBad& s2)
{
return s2<s1;
}
bool operator==(const StringBad& s1,const StringBad& s2)
{
return (std::strcmp(s1.str,s2.str)== 0);
}
- 用<运算符来表示>运算符,对于内联函数是个很好的选择。
- 将比较函数用作友元函数,有助于string对象与常规C字符串进行比较。如if(“loving”== str) 【str为类对象】
中括号表示法访问字符
char& StringBad::operator[](int n)
{
return str[n];
}
1.假设opr是一个类对象,opr[4]就被转换为opr.operator,将访问第五个字符。
2.还可以opr[5] = 'r',转换为opr.operator = 'r' 。这样代码访问了私有成员,但由于类方法,所以可以访问。
3.如果有个const常量, 上面方法不确保不会修改,所以另外定义一个函数const char& operator[](int n) const。C++将根据常量和非常量的特征标来区分函数。
const char& StringBad::operator[](int n) const
{
return str[n];
}
静态成员函数
- 可以将成员函数声明为静态,则有两个后果
1.不能通过对象调用静态成员函数。静态成员函数不能使用this指针。如果静态成员函数声明在公有部分,则可以使用类名加作用域解析运算符来使用它。方法在类声明中声明和定义如下:
static int Howmany() { return num_str;}
2.静态成员函数可以访问静态成员,但不能访问其他私有成员,因为不与特定对象相关联。
重载赋值运算符
若有如下代码:
StringBad s;
char temp[40];
cin.getline(temp,40);
s = temp;
最后一句工作原理:
1.使用构造函数StringBad(const char * )来创建一个临时对象,其中包括temp字符串副本。
2.使用之前声明的StringBad& operator=(const StringBad& )来进行复制。
3.析构函数删除临时对象。
这样效率太低了,所以可以直接重载赋值运算符
数组名是char*
StringBad& StringBad::operator=(const char* s)
{
delete [] str;
len = std::strlen(s);
str = new char[ len +1];
std::strcpy(str,s);
return *this;
}
重载>>运算符
1.同重载<<运算符一样,要返回istream类引用。假定输入不得超过最大字数CINLIM(在private部分定义:static const int CINLIM = 80;)
2.if条件下,如果文件到达末尾或读取的是空行,导致输入失败,is将设置为false
std::istream& operator<<(std::istream& is, StringBad& s)
{
char temp[StringBad::CINLIM];
is.get(temp,StringBad::CINLIM);
if(is)
st = temp;
while(is && is.get() !='\n')
continue;
return is;
}
程序设计
该程序允许输入几个字符串,然后存储到StringBad对象中,并显示他们,并指出哪个字符串最短,哪个字符串按字母顺序排在最前。
#include"stringbad.h"
using std::cout;
using std::endl;
using std::cin;
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
StringBad name;
cout<<"What's your name?"<<endl;
cin>>name;
cout<<name<<"输入不超过"<<ArSize<<"句的短句(空行退出): "<<endl;
StringBad 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 is your saying: "<<endl;
for(i=0;i<total;i++)
{
cout<< sayings[i][0]<<": "<<sayings[i]<<endl;
cout<<"length; "<<sayings[i].length()<<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: "<<sayings[shortest]<<endl;
cout<<"First: "<<sayings[first]<<endl;
cout<<"There are "<<StringBad::Howmany()<<" StringBad object"<<endl;
}
else
cout<<"no sayings\n";
return 0;
}
程序解析:
- 使用了类对象
if(!cin || temp[0]=='\0')
输入失败或空行会退出循环
构造函数使用new注意事项:
- 如果在构造函数使用new来初始化指针,则在析构函数使用delete
- new和delete要对应。new对应delete,new [] 对应delete []
- 如果有多个构造函数,必须以相同的方式使用new,因为只有一个析构函数。默认构造函数将它设置为nullptr或0