C++ How to Program

运算符重载:string类

主要包含string类的一些函数,具体可以看黑马的STL部分,这里不重复了。

主要注意以下部分:

string s4(s1);
//这是调用string类的拷贝构造函数,并s1的副本对其进行初始化。

s4=s2;
//这是string类重载的"="运算符,可以进行正确的赋值操作,正确处理自我赋值的情况,详情以后补充。

string s;
cout<<s[0]<<" "<<s[1];
//这里是重载[]来访问string中的元素,注意这不会进行任何越界检查(也就是有越界的风险)

cout<<s.at(0)<<s.at(1);
//at函数与上述[]一样,但是会检查是否在有效范围之内(越界会抛出error)

运算符重载的基本知识

通过像往常编写非静态成员函数或者非成员函数定义就可以实现运算符的重载,只不过现在的名字是operator加上重载的运算符。这些函数必须由类的对象来调用,并作用到这个对象上。

有如下的三个例外: 1、绝大多数类可以使用=对其数据成员进行逐个赋值操作,但是我们前面说到,如果数据成员包含指针,这将带来严重的问题。

因此,我们将显式地重载赋值运算符。

2、取址(&)运算符返回对象的地址,这个运算符也能被重载。

3、逗号(,)运算符从左到右对表达式进行求值,并返回表达式的值,也可以重载。

不能被重载的运算符

以下四个

1 2 3 4
. .* :: ?:

第二个是pointer to member。

重载运算符的规则和限制

1、优先级不变。(当然圆括号可以改变表达式中重载运算符的求值顺序)

2、结合性不变。(如果是从左到右结合,那么它的重载版本仍是这样)

3、元数不变。(也就是一元运算符,还是二元运算符保持不变,特别的是&,+,-,*即可作为一元、也可以作为二元,这些一元和二元的版本都可以被单独重载)。

4、不能创造新的运算符,只能重载现有运算符。

5、运算符作用在基本类型上的方法不可以被重载改变(比方说+不可以称之为两个int相减)。

运算符重载仅限于用户定义类型或者用户定义类型和基本类型的混合。

6、关系运算符必须被单独重载。

7、重载()、[]、->或者任何赋值运算符时,重载函数都必须声明为类成员,其他无所谓。

建议:

对类类型进行运算符重载,应该使得重载的运算符尽可能仿效内置运算符对基本类型的作用方式。

二元运算符的重载

有两种: 1、带有一个参数的非静态成员函数。

2、带有两个参数的非成员函数(需要被声明为类的友元)。

作为成员函数的二元重载运算符

仅当左操作数是该类的对象,且重载函数是一个成员时,二元运算符的重载函数才能作为成员函数。

注意左操作数是调用该成员函数的对象。

作为非成员函数的二元重载运算符

必须带有两个参数,其中一个必须是与重载运算符有关系的类对象或者是类对象的引用,例子如下:

bool operator<(const String&,const String&);

重载二元流插入运算符和流提取运算符

可以实现用户自定义的输入和输出。

这里通过例子简单见到介绍一下注意事项:

ostream &operator<<(ostream &output,const PhoneNumber &number)
{
output<<"("<<number.areaCode<<")"
<<number.exchange<<"-"<<number.line;
return output;
}

istream &operator>>(istream &input,PhoneNumber &number)
{
input>>setw(4)>>number.areaCode;
input>>setw(5)>>number.exchange;
input>>setw(6)>>number.line;
return input;
}

注意到函数的返回值都是引用,这样才可以实现级联调用(这和前面的串联调用很像):

PhoneNumber t1,t2; 
cin>>t1>>t2;

因为该类的对象在全局中只能有一个,所以参数列表中的第一个参数都是以引用的形式传递的(流对象不能复制,只能引用)。

ostream也是同理,注意只能用非成员函数实现,因为如果用非静态成员函数实现:那么调用该成员函数的对象将位于重载运算符的左边,与实际要求矛盾。

这里的setw限制了读到每个字符数组的字符个数,如setw(n)设置允许读入n个字符。

istream还有一个ignore函数:

istream &input;
input.ignore()
//将圆括号、空格、破折号等字符全部跳过。
//ignore丢弃输入流中指定个数的字符(默认为一个字符)。

重载流插入(<<)运算符

和上面基本一致。

作为非成员友元函数的重载运算符

这个已经在前面解释过了,不再多解释。

为什么流插入和流提取运算符必须被重载为非成员函数

这个已经在前面解释过了,不再多解释。

假设一定要用成员函数,那么得修改C++标准库中的数据类型,这是非常危险的(也不允许)。

重载一元运算符

有两种(就是上面说的两种)。

作为成员函数的一元重载运算符

以重载!为例

bool operator!()const;

这样,当编译器遇到表达式!s时,将会生成函数调用s.operator!()。

作为非成员函数的一元重载运算符

bool operator!(const String &)

!s将被处理为operator!()。

重载一元前置和后置运算符:++和--

自增自减运算符的前置和后置形式都可以进行重载,下面以自增为例介绍如何区分前置和后置形式。

为了使得编译器能够正确地识别要使用的是哪一种++模式,每个重载地运算符函数都必须有各自明显的特征。

假设前置的自增运算符为我们Date对象d1的天数+1。

//函数原型
Date &operator++;

//非静态成员函数
//当编译器遇到:++d1时,等价于
d1.operator++()。

//非成员函数类型
//编译器遇到++d1,等价于
operator++(d1)

//在类中的声明为
Date &operator++(Date &);

重载后置的自增运算符

非静态成员函数类型
//编译器遇到表达式d1++时。
//调用如下:
d1.operator++(0)

//对应的函数原型为
Date operator++(int)

这里的0其实是个哑巴,只是为了让编译器能够区分前置和后置而已。

非成员函数类型
//函数调用
operator++(d1,0);

//函数原型
Date operator(Date &,int);

这里的0作用和上面一样。

注意:后置的运算符按值返回对象,而前置的运算符按引用返回对象。这是因为在自增前,后置的运算符通常先返回一个包含对象原始数据的临时对象,C++将其作为右值,不能用在赋值运算符的左侧。前置的自增运算符返回实际自增后的具有新值的对象,可以放在赋值运算符的左侧。

#include<bits/stdc++.h>
#define ll long long
#define ld long double
using namespace std;
const double PI=acos(-1);
const int MAXN=2e5+10;
const ll mod=1e9+7;
const ll INF=1e9;
class node{
public:
node(){
num=1;
}
node operator++(int){
node tmp(*this);
num=num+1;
return tmp;
}
void show(){
cout<<this->num;
}
private:
int num;
};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
node a;
node b;
b=a++;
b.show();
//输出为1
return 0;
}

建议:使用前置的自增或者自减运算符,后置会对性能造成更大的影响。

实例研究:Date类

(跳过)

注意不能返回局部变量的引用,否则会编译错误(因为可能该对象被析构了)。

动态内存管理

通过运算符new和delete实现。

可以使用new运算符在执行期间为对象或者数组动态分配所需要的内存,对象和数组被存储在堆中(这是一个专门用来存储动态分配对象的内存区域)。一旦内存中的自由存储区被分配,那么就可以通过new运算符返回的指针进行访问。当不再需要内存时,则可以使用delete来释放内存,从而内存返回给堆区,以便供将来的new操作复用。

使用new来动态获取内存

Time *timePtr=new Time();

上面的new运算符为一个Time类型的对象分配大小合适的内存空间,调用默认的构造函数来初始化这个对象并返回一个指向new运算符右侧类型的指针(也即是Time*)。

如果new无法在内存中为该对象找到足够的空间,则会抛出异常(一般会抛出一个bad_alloc异常)。

使用delete动态释放内存

delete timePtr;

这将先调用timePtr指向对象的析构函数,早收回对象所占用的内存空间,把内存返回给自由存储区。

注意:

1、当动态内存分配的空间不再使用时,要及时释放。

2、不要删除不是new分配的内存。

3、delete一个动态分配的内存块后确保不要对同一块内存再次delete,一个建议是:立即将delete过的指针的值设置为nullptr,delete一个nullptr是没有影响的。

补充:可以重载new和delete运算符,两者必须在同一作用域内重载。(以后再详细补充)

动态内存的初始化

可以为新建立的基本类型变量提供初始化值:

double *ptr=new double(3.14);

同样的语法也可以用来将由逗号分隔开的参数列表指定给对象的构造函数

Time *timePtr=new Time(12,45,0);

使用new[]动态分配内置数组

int *Array=new int[10]();

这声明了一个指针Array,并将指向一个动态分配的10元素的整数数组第一个元素的指针赋给它。

new int[10]值后面的括号初始化数组元素——基本数据类型设置为0,bool为flase,指针为nullptr,对象则通过其默认构造函数进行初始化,注意:在编译时确定数组的大小时必须用常量整数表达式确定,动态分配数组的大小可以用在执行期间求值的任何非负整数表达式来指定。

#include<bits/stdc++.h>
#define ll long long
#define ld long double
using namespace std;
const double PI=acos(-1);
const int MAXN=2e5+10;
const ll mod=1e9+7;
const ll INF=1e9;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int a=0;cin>>a;
int *ptr=new int[a];
for(int i=0;i<a;i++){
cin>>*(ptr+i);
}
for(int i=0;i<a;i++){
cout<<*(ptr+i)<<" ";
}
return 0;
}

//样例
6
1 2 3 4 5 6
//输出
1 2 3 4 5 6

C++11使用列表初始化动态分配内存的数组

int a=new int[10]{1,2,3,4,5,6,7,8,9,10};

默认初始化列表——对基本类型来说,就是将每个元素置为0。

使用delete[]动态释放内置数组

delete []Array;

注意:如果上面语句中的指针指向一个内置数组,则语句先调用数组中每个对象的析构函数,后回收空间。

如果不包含[],且指针指向一个对象的数组,则结果是未知的,可能会只为数组的首对象调用析构函数。

对空指针进行delete,或者delete[]操作并无任何效果。

C++11使用unique_ptr管理动态分配的内存

unique_ptr是一个智能指针,当一个unique_ptr超出范围时,它的析构函数将自动把其管理的内存返还到自由存储区中。

实例研究:Array类

(跳过)

运算符作为成员函数或非成员函数的比较

一个特定类的运算符成员函数仅在以下两种情形中被调用,当二元运算符的左操作数是该类的对象时,或者一元运算符唯一的操作数是该类的对象时。

可交换的运算符

一般由非成员函数实现,这样可以使得运算符具有交换性。

类型转换

程序员必须指出如何转换,编译器才能执行。

转换运算符

又称为强制类型转换运算符,可用于将某一类的对象转换为另外一个类的对象,这种转换运算符必须是非static成员函数。

MyClass::operator char *()const

可以将用户自定义吧类型Myclass的对象转换为一个临时的char*对象,这个运算符函数声明为const,因为不修改原始的对象。

不需要指定转换类型,因为返回类型就时目标类型。

当编译器遇到:

static_cast<char*>(s);

会产生函数调用:

s.operator char*();

这将把操作数转换为char*类型。

重载强制类型转换运算符

MyClass::operator int()const;
MyClass::operator OtherClass()const;

强制类型转换运算符和转换构造函数的隐式调用

两者优点之一是:必要时,编译器可以隐式地调用这些函数来创建临时的对象。

如:String类的对象s出现在本来一个应该出现字符数组char*的位置上,那么

cout<<s

编译器可以使用重载的强制类型转换运算符operator char将对象转换为char ,并在表达式中使用这个转换结果char *。

这样就可以不必重载<<运算符了。

explicit构造函数与转换运算符

任何单参数且不被声明为explict的构造函数可以被编译器识别用来进行隐式类型转换,除了拷贝构造函数。

构造函数中的实参被转换为函数中定义的类对象,这种转换是自动进行的。

但某种情况下,这样会带来麻烦和问题。从而产生歧义。

Array integers(7);
outArray(integers);
outArray(3);//发生隐式类型转换

void outArray(const Array &arrayOutPut){
......
}

默认情况下数组中的元素都为0。

本来,第一行是创建了一个integers的Array类对象(有7个元素,默认为0),然后输出。

但是由于没有提供接受一个int作为参数的outArray函数,所以编译器先确定Array类是否提供了一个将int转换为Array的转换构造函数,由于任何接受单参数的构造函数都可以被认为是转换构造函数,所以编译器认为接受单个int参数的Array构造函数是一个转换构造函数,并将参数3转换为一个包含了3个元素的临时Array对象,然后再将它传递给函数outArray,输出内容。乌龙就这么产生了。

为了解决这个问题,所以有了关键字explicit。

防止无意中调用将单参数的构造函数作为转换构造函数

我们在声明每个单参数的构造函数时,前面加上explicit关键字,目的是禁止不应该允许的由转换构造函数完成的隐式转换。

这样上面的程序应该修改为:

Array integers(7);
outArray(integers);
outArray(3);//不会发生隐式类型转换,会报错

outArray(Array(3))//显式转换

void outArray(const Array &arrayOutPut){
......
}

这样就解决了问题。

建议在单参数的构造函数中总是使用explicit关键字,除非打算将它们用作转换构造函数。

C++11:explicit转换运算符

同样,也可以声明explicit的转换运算符来防止编译器使用它们进行隐式转换。

explicit MyClass::operator char*()const 

重载函数调用运算符

这个非常灵活多变,也非常重要,因为函数能够接受任意数量的逗号分隔参数。

举个例子:

String String::operator()(size_t index,size_t length) const

这里想要的是检查起始位置越界或者负长度的功能,注意必须是一个const成员变量,不应该修改原始的String对象。

String s1="AEIOU";
s1.operator(2,3);

这将会得到字串"IOU"。