【cpluscplus教程翻译】Classes (II)

发布时间 2023-05-22 18:27:49作者: xiaoweing

操作符重载

本质上,类就是在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 (&param == 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> { ... };

第一个是通用类模板,第二个是特化
当我们声明特化时,必须定义想要的所有成员,即使是和通用类一样的,因为特化类和模板类没有继承关系