18 类成员的访问权限以及类的封装

1 要解决的问题

我们来看这样一个例子:

#include<iostream>

using namespace std;

class Student {
public:
    string name;
    int age;
    int score;

    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}

    void say() {
        cout << "我是" << name << ",我今年" << age << "岁了,我的成绩是" << score << "分。" << endl;
    }
};

int main() {
    Student stu1("张三", 16, 85);
    stu1.age = -16;
    stu1.say();
    return 0;
}

main() 函数中,我们错误地将学生对象的年龄属性设置为了 -16 ,那么很显然,我们最终会得到错误的输出结果:“我是张三,我今年-16岁了,我的成绩是90分。”。

2 发掘问题原因

在上面的例子中,我们并没有对 Student 进行合理的封装,这使得 Student 类的所有成员,都可以自由地在对应的类内和类外被使用。这也正是 public(公共的) 关键字的含义,此关键字下的所有成员,可以自由地在本类内外被使用。

这会带来两个糟糕的后果:

  1. 类的成员可以不受限制地被修改,这使数据的安全性和程序稳定性变得糟糕。
  2. 写代码时,如果要操作一大堆变量,会使程序更加复杂,也就是说,这会使这个类更不容易使用。

3 解决方法

我们需要使用限制 类成员的访问权限 的关键字,privateprotectedpublic ,对类进行 封装

  1. private 修饰的属性和方法,只能在本类中使用;
  2. protected 修饰的属性和方法,只能在本类和本类的派生类中被使用;
  3. public 修饰的属性和方法,可以在本类内外自由使用。
  1. 这三个关键字可以以如何顺序出现,也可以出现多次,每种权限作用到另一个权限出现或者类体结束。
  2. 如果未出现这些权限关键字,默认访问权限为 private
  3. C++ 中,结构体与类功能几乎相同,重要区别是,结构体的默认访问权限为 public

此时,类可以改成这样:

class Student {
private:
    string name;
    int age;
    int score;
public:
    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}

    void say() {
        cout << "我是" << name << ",我今年" << age << "岁了,我的成绩是" << score << "分。" << endl;
    }
};

这样,age 属性就不能在类外被访问,自然不能被任意修改。

根据C++软件设计规范,实际项目开发中的成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public。

4 访问私有属性

现在,我们的 Student 类,已经不允许使用者随意将 age 属性改为负数了,但是,使用者也不能做到正常地更新年龄信息了。

在很多情况下,我们是需要修改类的私有属性的,因此,我们需要 可控的访问和修改私有属性的方法

有两种解决方法:友元Getter 与 Setter

4.1 友元(friend)(了解即可)

friend 的意思是朋友,或者说是好友,与好友的关系显然要比一般人亲密一些。我们会对好朋友敞开心扉,倾诉自己的秘密,而对一般人会谨言慎行,潜意识里就自我保护。在 C++ 中,这种友好关系可以用 friend 关键字指明,中文多译为“友元”,借助友元可以访问与其有好友关系的类中的私有成员。如果你对“友元”这个名词不习惯,可以按原文 friend 理解为朋友。

友元 是通过规定一个函数或一个类拥有访问这个类的私有成员的权限的方式,实现在类外访问类的私有成员的。把类看做一个城堡,类的私有成员就是被城堡包含的贵族,而友元的处理方式,就相当于给每个需要访问类的私有成员的函数或类授予进出城堡的特别通行证。

我并不推荐大家使用友元,首先,它破坏了类的封装;而且,更重要的是,在软件开发过程中,随着开发进度的推进以及开发需求的变更,有些函数或类,不再需要访问此类的私有成员,此时需要回收 “特别通行证” ,也会有新的函数和类,需要访问此类的私有成员,此时就需要发放新的 “特别通行证” ,这会给软件的开发和维护带来很多麻烦。

4.1.1 将非成员函数声明为友元函数

语法: friend 函数原型声明;

例子:

#include<iostream>

using namespace std;

class Student {
private:
    string name;
    int age;
    int score;
public:
    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}
    // 声明 say 函数为友元函数
    friend void say(Student& s);
};

void say(Student &s) {
    cout << "我是" << s.name << ",我今年" << s.age << "岁了,我的成绩是" << s.score << "分。" << endl;
}

int main() {
    Student stu1("张三", 16, 85);
    // 使用友元函数
    say(stu1);
    return 0;
}

说明:友元函数声明位置可以在 privateprotectedpublic 区域中的任一个,效果相同。

4.1.2 将其他类的成员函数声明为友元函数

语法: friend 其他类::函数原型声明;

例子:

#include<iostream>

using namespace std;

// 必须先声明 Student 这个类名,否则编译器不知道 void say(Student &s); 处的 Student 是什么
class Student;

class A{
public:
    // 必须将函数定义在 Student 类的具体声明之后,否则编译器不知道 name 、age 等属性是什么
    void say(Student &s);
};

class Student {
private:
    string name;
    int age;
    int score;
public:
    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}
    // 声明 A 类中的 say 方法为友元函数
    friend void A::say(Student& s);
};

void A::say(Student &s) {
    cout << "我是" << s.name << ",我今年" << s.age << "岁了,我的成绩是" << s.score << "分。" << endl;
}

int main() {
    Student stu1("张三", 16, 85);
    A a;
    a.say(stu1);
    return 0;
}

运行结果
我是张三,我今年16岁了,我的成绩是85分。

4.1.2 友元类

不仅可以将一个函数声明为一个类的“朋友”,还可以将整个类声明为另一个类的“朋友”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数。

语法: friend class 类名;

例子:

#include<iostream>

using namespace std;

// 必须先声明 Student 这个类名,否则编译器不知道 void say(Student &s); 处的 Student 是什么
class Student;

class A{
public:
    // 必须将函数定义在 Student 类的具体声明之后,否则编译器不知道 name 、age 等属性是什么
    void say(Student &s);
};

class Student {
private:
    string name;
    int age;
    int score;
public:
    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}
    // 声明友元类 A
    friend class A;
};

void A::say(Student &s) {
    cout << "我是" << s.name << ",我今年" << s.age << "岁了,我的成绩是" << s.score << "分。" << endl;
}

int main() {
    Student stu1("张三", 16, 85);
    A a;
    a.say(stu1);
    return 0;
}

运行结果
我是张三,我今年16岁了,我的成绩是85分。

特别说明:

  1. 友元类的关系是单向的,不是双向的。
    如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类
  2. 友元类的关系不能传递。
    A 有朋友 B ,B 有朋友 C ,不代表 A 和 C 是朋友。

4.2 Getter 与 Setter

Getter 即外部访问类的私有成员需要使用的 访问器Setter 即外部更改类的私有成员需要使用的 更改器 。它们的访问权限应该是 public (公共的)。

setter和getter的命名约定,如 getXxx() 和 setXxx(),其中 Xxx 变量的名称。

例子:

#include<iostream>

using namespace std;

class Student {
private:
    string name;
    int age;
    int score;
public:
    Student(string _name, int _age, int _score) : name(_name), age(_age > 0 ? _age : 0), score(_score) {}
    // age 属性的 Getter 方法
    int getAge(){
        return age;
    }
    // age 属性的 Setter 方法
    void setAge(int newAge){
        age = newAge > 0 ? newAge : age;
    }
    void say() {
        cout << "我是" << name << ",我今年" << age << "岁了,我的成绩是" << score << "分。" << endl;
    }
};

int main() {
    Student stu1("张三", 16, 85);
    stu1.setAge(-16);
    stu1.say();
    return 0;
}

此时,在 main 函数中,使用者依然企图胡乱地修改 age 属性的值,但因为有了 Setter 方法的保护,将年龄修改为负数的指令并没有被执行。