操作符重载
本质上,类就是在C++代码里定义了新的类型,在代码中,类型不仅用来构造和赋值,还可以用操作符进行运算,考虑基础类型的加减乘除
int a, b, c;
a = b + c;
上面这个例子用了加法操作符和赋值操作符,对于基础类型,这些操作的含义非常显而易见且无歧义,但是对自定义类型来说,不见得是同一回事
struct myclass {
string product;
float price;
} a, b, c;
a = b + c;
显然,b和c的加法具体干什么不得而知,事实上,这段代码会编译报错,因为myclass没有定义加法的行为,然后C++允许重载大多数操作符,不光是类,基础类型也可以,下面是可以重载的操作符
使用operator函数就可以重载操作符,操作符函数和普通函数的形式差不多,只不过函数名需要用operator关键字开头,然后才是操作符,语法为type operator sign (parameters) { /*... body ...*/ }
考虑笛卡尔坐标系下的向量,两个向量相加可以认为是对应的x和y坐标相加,例如向量(3,1)和向量(1,2)相加的结果是(3+1, 1+2)=(4,3),对应的代码为
// overloading operators example
#include <iostream>
using namespace std;
class CVector {
public:
int x,y;
CVector () {};
CVector (int a,int b) : x(a), y(b) {}
CVector operator + (const CVector&);
};
CVector CVector::operator+ (const CVector& param) {
CVector temp;
temp.x = x + param.x;
temp.y = y + param.y;
return temp;
}
int main () {
CVector foo (3,1);
CVector bar (1,2);
CVector result;
result = foo + bar;
cout << result.x << ',' << result.y << '\n';
return 0;
}
CVector出现了这么多次,容易让人困惑,有些是类名,有些函数返回类型,有些是构造函数名,我们解析一下
CVector (int, int) : x(a), y(b) {} // function name CVector (constructor)
CVector operator+ (const CVector&); // function that returns a CVector
加法操作符重载后,+的效果和显式调用本质上是一样的
c = a + b;
c = a.operator+ (b);
重载的运算符实际上可以有任何行为,不一定需要和数学含义一致,例如加法运算符可以实际上是减法,判断运算符实际上可以是填零,不过推荐和数学含义一致。
重载运算符函数的参数很自然地被认为是操作符的右边部分,对于二元运算符来说,这很常见(操作符左右各一个操作数)。不过运算符可以有各种各样的形式,下面是汇总表格(@替换成对应的操作符)
需要注意的是:操作符既可以作为成员函数也可以作为非成员函数进行重载,非成员函数形式重载需要注意参数类型及个数(本质上是命令空间发生了变化)
// non-member operator overloads
#include <iostream>
using namespace std;
class CVector {
public:
int x,y;
CVector () {}
CVector (int a, int b) : x(a), y(b) {}
};
CVector operator+ (const CVector& lhs, const CVector& rhs) {
CVector temp;
temp.x = lhs.x + rhs.x;
temp.y = lhs.y + rhs.y;
return temp;
}
int main () {
CVector foo (3,1);
CVector bar (1,2);
CVector result;
result = foo + bar;
cout << result.x << ',' << result.y << '\n';
return 0;
}
this关键字
关键字this表示成员函数正在执行的哪个对象的指针,主要用在成员函数里表示当前对象(思考类对象模型,哪些放在对象里,哪些放在类里)
用法之一是确定参数是不是对象本身(节省返回开销)
// example on this
#include <iostream>
using namespace std;
class Dummy {
public:
bool isitme (Dummy& param);
};
bool Dummy::isitme (Dummy& param)
{
if (¶m == this) return true;
else return false;
}
int main () {
Dummy a;
Dummy* b = &a;
if ( b->isitme(a) )
cout << "yes, &a is b\n";
return 0;
}
也常用在赋值运算符中,因为赋值运算符返回的是对象的引用(实现链式运算a=b=c=d),继续研究笛卡尔向量,它的赋值运算符函数可以是
CVector& CVector::operator= (const CVector& param)
{
x=param.x;
y=param.y;
return *this;
}
事实上,编译器隐式生成的代码就和上面差不多
静态成员(static members)
类也可以有静态成员,数据或函数
类的静态成员变量也叫做类变量,因为这个类的所有对象共用这一个变量
常见用法是,用类变量记录有多少个对象
// static members in classes
#include <iostream>
using namespace std;
class Dummy {
public:
static int n;
Dummy () { n++; };
};
int Dummy::n=0;
int main () {
Dummy a;
Dummy b[5];
cout << a.n << '\n';
Dummy * c = new Dummy;
cout << Dummy::n << '\n';
delete c;
return 0;
}
事实上,静态成员和非静态成员是一样的属性,只不过静态成员的作用域是类,因为这个原因,并且避免声明多次,他们不能再类里初始化,必须要在类外面int Dummy::n=0;
因为静态变量由所有对象共享,因此任何对象都可以访问这个变量,甚至只通过类名就行
cout << a.n;cout << Dummy::n;
这两句话使用的是同样的变量
静态成员函数也是一样的:可以有所有对象访问,与非静态成员函数不同,静态成员函数不能访问非静态成员变量(显然,如果通过类名使用,根本没有对象,就没办法访问非静态成员变量,没有this指针)
常成员函数(const member functions)
当一个对象是常量const MyClass myobject;
使用它的成员变量必须确保只读,就好像它的所有成员变量都是常量,需要注意的是,构造函数被允许调用并修改这些成员变量(可以认为构造函数的函数体实际上是赋值运算符,初始化列表才是真正的构造过程)
// constructor on const object
#include <iostream>
using namespace std;
class MyClass {
public:
int x;
MyClass(int val) : x(val) {}
int get() {return x;}
};
int main() {
const MyClass foo(10);
// foo.x = 20; // not valid: x cannot be modified
cout << foo.x << '\n'; // ok: data member x can be read
return 0;
}
常量对象的成员函数只有被指定成常成员函数才可以被调用(上面只保证不在外面被修改,如果是成员函数内被修改呢,这个是运行期过程,需要在编译器加约束才能避免),上面这个例子中,get函数因为没有用const修饰,因此foo对象不能调用get函数,声明一个函数为常成员函数的语法为int get() const {return x;}
需要注意的是,const也可以用来修饰返回值,不要被各种const混淆
int get() const {return x;} // const member function
const int& get() {return x;} // member function returning a const&
const int& get() const {return x;} // const member function returning a const&
常成员函数不能修改非静态成员变量也不能调用其他非常成员函数,本质上,常函数不能修改对象的状态
常对象只能调用常成员函数,非常对象没有这个限制。
你可能会认为你很少声明常对象,因此认为把所有不修改对象的成员函数标记为常成员函数没有意义,但是实际上常对象很常见,大多数参数为类的函数实际上接收的是常引用(避免开销),因此这些函数只能使用他们的常成员
// const objects
#include <iostream>
using namespace std;
class MyClass {
int x;
public:
MyClass(int val) : x(val) {}
const int& get() const {return x;}
};
void print (const MyClass& arg) {
cout << arg.get() << '\n';
}
int main() {
MyClass foo (10);
print(foo);
return 0;
}
同上分析,如果get不标记为常,那么print函数无法实现
成员函数也可以基于常量进行重载:例如两个函数参数相同,但是一个是常函数一个不是,那么常函数版本只能由常对象调用,非常对象可以调用非常版本
// overloading members on constness
#include <iostream>
using namespace std;
class MyClass {
int x;
public:
MyClass(int val) : x(val) {}
const int& get() const {return x;}
int& get() {return x;}
};
int main() {
MyClass foo (10);
const MyClass bar (20);
foo.get() = 15; // ok: get() returns int&
// bar.get() = 25; // not valid: get() returns const int&
cout << foo.get() << '\n';
cout << bar.get() << '\n';
return 0;
}
类模板(class template)
就像函数模板一样,也可以新建类模板,如下
template <class T>
class mypair {
T values [2];
public:
mypair (T first, T second)
{
values[0]=first; values[1]=second;
}
};
我们定义的这个类可以存储两个相同类型的值,例如,如果想存两个整数mypair<int> myobject (115, 36);
或者两个浮点数mypair<double> myfloats (3.0, 2.18);
构造函数是唯一的成员函数,因此在类里直接作为内联函数,如果在类模板外定义,需要加上templlate<>前缀
// class templates
#include <iostream>
using namespace std;
template <class T>
class mypair {
T a, b;
public:
mypair (T first, T second)
{a=first; b=second;}
T getmax ();
};
template <class T>
T mypair<T>::getmax ()
{
T retval;
retval = a>b? a : b;
return retval;
}
int main () {
mypair <int> myobject (100, 75);
cout << myobject.getmax();
return 0;
}
注意语法(之所以这样的语法 scope避免重复)
template <class T> T mypair<T>::getmax ()
第一个T是模板参数,第二个T是函数返回值,第三个T用来标记实例化的类(避免命名冲突)
模板特化(template specialization)
可以在模板参数为不同类型时,修改模板的定义,这个被称为模板特化
例如mycontainer是一个模板类,只存一个元素,且只有一个函数increase,我们想在存char时增加一个大写函数uppercase,例子如下:
// template specialization
#include <iostream>
using namespace std;
// class template:
template <class T>
class mycontainer {
T element;
public:
mycontainer (T arg) {element=arg;}
T increase () {return ++element;}
};
// class template specialization:
template <>
class mycontainer <char> {
char element;
public:
mycontainer (char arg) {element=arg;}
char uppercase ()
{
if ((element>='a')&&(element<='z'))
element+='A'-'a';
return element;
}
};
int main () {
mycontainer<int> myint (7);
mycontainer<char> mychar ('j');
cout << myint.increase() << endl;
cout << mychar.uppercase() << endl;
return 0;
}
特化的语法 template <> class mycontainer <char> { ... };
,模板参数列表为空,这是因为所有的参数都被特化了,
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };
第一个是通用类模板,第二个是特化
当我们声明特化时,必须定义想要的所有成员,即使是和通用类一样的,因为特化类和模板类没有继承关系