只读成员函数

回到我们的运算符重载。这一次我们要给 String 重载 == 这个运算符,指明两个字符串是相等的。

bool String::operator==(const String& rhs) {
    if (len != rhs.len) return false; // 前提是长度得相等
    for (unsigned i{0}; i < len; i++) {
        if (str[i] != rhs.str[i])
            return false;             // 如果某一位不相等则不等
    }
    return true;
}

函数的工作原理很简单。注意从这里开始我们就用类外定义的形式来写成员函数了,减少一些篇幅(所以如果你在跟着操作的话,请不要忘记在类内添加上对应的声明)。

然后我们尝试使用它。

#include <iostream>
int main() {
    String a("Hello"); // 你可以自己改变这个初始化值,看看效果变化
    // 下面的 "Hello" 通过两次隐式转换到 String,然后调用 == 重载
    if (a == "Hello") {
        std::cout << "Hi" << std::endl;
    }
}

现在看上去没有问题,当 a"Hello" 的时候输出,不相等的时候不输出。那么接下来我们想扩展这段程序,把判断相等的部分挪到一个单独的函数中去。

#include <iostream>
// 只需要 x 的值,所以用只读引用防止复制开销和更改
bool isOk(const String& x) {
    return x == "Hello" || x == "Hi";
}
int main() {
    String a("Hello");
    if (isOk(a)) {
        std::cout << "How are you?" << std::endl;
    }
}

感觉没问题,但真有问题。你会发现编译过不了了。为什么呢?问题就出在 isOk 的参数 x 是带 const 限定的。

假如我有一个类 A,它有一个成员数据 data 和一个成员函数 change

struct A {
    int data;
    void change() {
        data++;
    }
};
int main() {
    const A a;
//  a.change();  // 编译错误
}

那么,在 main 函数里面声明并定义只读的 A 类型对象 a。如果 achange 能被调用的话,那么 adata 成员就会发生更改,这就不符合“只读”的含义了。所以默认情形下不能调用只读对象的成员函数

如果想要一个只读对象的成员函数可以被调用,需要给这个函数整体加上 const 限定。它的语法是:

返回值类型 类名::成员函数名(参数列表) const 函数体

也就是说把 const 关键字加在参数列表和函数体之间。这种被整体 const 限定的成员函数称为只读成员函数。比如:

struct A {
    int data;
    void change() const {
//      data++; // 编译错误
    }
};

很显然,只读成员函数内部不能对成员数据做更改,也不能调用非只读的成员函数。也就是说,只读成员函数中的 this 类型是 const T* 的。

于是基于此,我们意识到原来的 String 类有许多函数是不会更改成员数据的,那么它们都应该整体加上 const 限定。比如刚刚的 operator==,又比如 operator+ length operator bool。于是给他们加上 const 限定就好了(声明和定义都需要)。

只读与非只读重载

现在问题来到了 operator[] 上。

char String::operator[](unsigned pos) {
    return str[pos];
}

首先我们先把返回值类型改成引用。否则的话,形如 a[0] = '@' 的表达式是没有效果的(这个原理类似之前返回 *this)。于是:

char& String::operator[](unsigned pos) {
    return str[pos];
}

那么问题就是:这个函数该不该被整体 const 限定呢?如果不设置为只读的,那么我就没法对只读对象调用 operator[] 了。但如果设置为只读的,那么 a[0] = '@' 还是编译不过:只读成员函数中 str[pos]const char 类型的,不能绑定到非只读的 char& 引用上。

char& String::operator[](unsigned pos) const {
    return str[pos]; // 编译错误:不能将只读类型绑定到非只读引用上去
}

最终的解决方法是利用函数重载,声明两个 operator[] 成员函数,分别对应只读和非只读的版本:

// 非只读版本
char& String::operator[](unsigned pos) {
    return str[pos];
}
// 只读版本
const char& String::operator[](unsigned pos) const {
    return str[pos];
}

然后,在调用 operator[] 时,编译器会优先选择整体带 const 限定的重载;如果不能,则选择非只读的。

int main() {
    const String a("Hello");
    String b("Hello");
    a[0]; // 调用只读版本
    b[0]; // 调用非只读版本
}

只有成员函数才可以整体(在最顶层)加 const 限定。比如你定义类型别名 using T = void()const;,那么 T 只能作为成员类型出现在类的定义里,不能在类外使用。

只读成员数据

最后,我们简单提一下只读成员数据。既然是只读的,它就没有办法通过任何方式被赋值:

struct A {
    const int data{0}; // 只读成员数据
    A() {
        data = 1;      // 错误:不能对只读类型赋值
    }
    void f() const {   // 这与函数 const 限定与否无关
        data = 1;      // 错误:不能对只读类型赋值
    }
};

而且,只读数据成员必须被显式地初始化,这和我们之前的写法是一致的:

int main() {
    const int a;     // 错误:只读对象必须被初始化
    const int b{42}; // OK
}

所以说,这就要求只读成员必须拥有一个默认成员初始化器,或者出现在成员初始化列表中:

struct A {
    const int data; // 如果这里不带默认初始化器的话……
    A(): data{42} { // 那么这里必须为它初始化
    }
};
最近更新:
代码未运行