Skip to content

3.2 访问控制

访问说明符

我们使用 访问说明符(access specifiers) 加强类的封装性:

  • 定义在 public 说明符之后的成员在整个程序内可被访问,public成员定义类的接口
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装(即隐藏)了类的实现细节。

一个类可以包含0个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或到达类的结尾处为止。

添加访问说明符的Sales_data.h
Sales_data.h
class Sales_data {
    public:
        // 添加了访问说明符
        Sales_data() = default;
        Sales_data(const std::string &s, unsigned n, double p) :
            bookNo(s), units_sold(n), revenue(p*n) { }
        Sales_data(const std::string &s) : bookNo(s) { }
        Sales_data(std::istream&);
        std::string isbn() const { return bookNo; }
        Sales_data &combine(const Sales_data&);

    private:
        // 添加了访问说明符
        double avg_price() const
        { return units_sold ? revenue/units_sold : 0; }
        std::string bookNo;
        unsigned units_sold = 0;
        double revenue = 0.0;
};

classstruct关键字

二者的唯一区别是,structclass的默认访问权限不太一样。

类可以在它的第一个说明符之前定义成员,若是struct则定义在第一个说明符之前的成员是public的;若是class,反之。

类的组合

在组合中,一个类(整体)包含另一个类(部分)的实例作为其成员。这种关系类似于“拥有”关系,即整体对象拥有部分对象。部分对象的生命周期由整体对象控制,当整体对象被销毁时,其包含的部分对象也会被销毁。

  • 不仅要负责对本类中的基本类型成员数据赋初值,也要对对象成员初始化。

    class Point {
    public:
        Point(int x = 0, int y = 0) : x(x), y(y) {}  // 初始化对象成员
        int x, y;
    };
    
    class Student {
    public:
        Student(const std::string& name, int x, int y)
            : name(name), address(x, y) {}  // 初始化基本类型成员和对象成员
        std::string name;
        Point address;
    };
    
  • 构造函数调用顺序:

    • 先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。
    • 然后调用本类的构造函数(析构函数的调用顺序相反) 。
  • 若调用默认构造函数(即无形参的),则内嵌对象的初始化也将调用相应的默认构造函数。
  • 如果要为组合类编写拷贝构造函数,则需要为内嵌成员对象的拷贝构造函数传递参数。

前向引用

前向声明(forward declaration)是一种在C++中声明类或函数的方式,它允许在不包含完整定义的情况下使用某个类或函数。前向声明通常用于避免循环依赖问题。

class Fred; // 前向声明

1-循环依赖问题

当两个类之间存在相互依赖时,就会出现循环依赖问题。例如,class A中包含class B的成员,而class B中又包含class A的成员。

class B; // 前向声明

class A {
public:
    void f(B b);
};

class B {
public:
    void g(A a);
};

在上述例子中,AB之间存在循环依赖。通过前向声明class BA类可以声明一个接受B作为参数的函数f,而不需要包含B的完整定义。

2-前向声明的限制

  • 不能声明对象:在提供完整的类定义之前,不能声明该类的对象。
  • 不能访问类的细节:在前向声明之后,不能在内联成员函数中使用该类的任何细节(如成员变量或成员函数)。
class Fred; // 前向声明

class Barney {
public:
    void method() {
        x->yabbaDabbaDo(); // 错误:Fred的完整定义尚未提供
    }
private:
    Fred* x; // 正确:可以声明指针
};

class Fred {
public:
    void yabbaDabbaDo(); // 正确:在完整定义中声明成员函数
private:
    Barney* y; // 正确:可以声明指针
};

3-前向声明的正确使用

  • 指针和引用:可以使用前向声明的类来声明指针或引用。
  • 函数参数和返回值:可以使用前向声明的类作为函数的参数或返回值类型。
class Fred; // 前向声明

class Barney {
public:
    void setFred(Fred* f); // 正确:使用指针
    Fred* getFred();       // 正确:返回指针
private:
    Fred* x; // 正确:声明指针
};

class Fred {
public:
    void setBarney(Barney* b); // 正确:使用指针
    Barney* getBarney();       // 正确:返回指针
private:
    Barney* y; // 正确:声明指针
};

4-完整代码示例

以下是一个完整的代码示例,展示了如何使用前向声明来解决循环依赖问题:

// 前向声明
class Fred;

class Barney {
public:
    void setFred(Fred* f) { x = f; }
    Fred* getFred() { return x; }
private:
    Fred* x; // 指针
};

class Fred {
public:
    void setBarney(Barney* b) { y = b; }
    Barney* getBarney() { return y; }
    void yabbaDabbaDo() { /* 实现细节 */ }
private:
    Barney* y; // 指针
};

int main() {
    Fred f;
    Barney b;
    b.setFred(&f);
    f.setBarney(&b);
    return 0;
}

友元

类允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的 友元(friend)

友元的声明(待整理)

struct X {
    friend void f() { /* 友元函数可以定义在类的内部*/ }
    X() { f(); }  // 错误:f 还没有被声明
    void g();
    void h();
};

void X::g() { return f(); }  // 错误:f 还没有被声明
void f();  // 声明那个定义在 X 中的函数
void X::h() { return f(); }  // 正确:现在 f 的声明在作用域中了

类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。

甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。

友元声明只能出现在类定义的任何地方,友元不是类的成员也不受它躲在区域访问控制级别的约束。

友元的声明仅仅指定了访问的权限,我们必须在友元声明之外再专门对函数进行一次声明。

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。

非成员函数的友元

如果类想把一个函数作为它的友元,只需要增加一条以 friend 关键字开始的函数声明语句即可。

添加友元的Sales_data.h
Sales_data.h
class Sales_data {
    // 为 Sales_data的非成员函数所做的友元声明
    friend Sales_data add(const Sales_data&, const Sales_data&);
    friend std::istream& read(std::istream&, Sales_data&);
    friend std::ostream& print(std::ostream&, const Sales_data&);
    // 其他成员及访问说明符与之前一致
    public:
        // 添加了访问说明符
        Sales_data() = default;
        Sales_data(const std::string &s, unsigned n, double p) :
            bookNo(s), units_sold(n), revenue(p*n) { }
        Sales_data(const std::string &s) : bookNo(s) { }
        Sales_data(std::istream&);
        std::string isbn() const { return bookNo; }
        Sales_data &combine(const Sales_data&);
    private:
        // 添加了访问说明符
        double avg_price() const
        { return units_sold ? revenue/units_sold : 0; }
        std::string bookNo;
        unsigned units_sold = 0;
        double revenue = 0.0;
};

// Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);

类之间的友元

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

  • 友元类:一个类可以被声明为另一个类的友元,从而访问其私有和保护成员。

  • 单向性:友元关系是单向的,除非显式声明为双向。

  • 声明方式:使用 friend 关键字在类中声明友元类。

class MyClass {
    friend class FriendClass;  // 声明友元类

private:
    int privateVar;
};

class FriendClass {
public:
    void accessPrivateVar(MyClass& obj) {
        obj.privateVar = 42;  // 可以访问 MyClass 的私有成员
    }
};

注意: 友元关系不存在递进性。

其他类的成员函数的友元

成员函数的友元 是指一个类的成员函数被声明为另一个类的友元,从而可以访问后者的私有和保护成员。

  • 访问权限:友元成员函数可以访问另一个类的私有和保护成员。

  • 声明方式:使用 friend 关键字在类中声明友元成员函数。

  • 单向性:友元关系是单向的,除非显式声明为双向。

class MyClass {
private:
    int privateVar;
};

class FriendClass {
public:
    void accessPrivateVar(MyClass& obj) {
        obj.privateVar = 42;  // 可以访问 MyClass 的私有成员
    }
};

// 声明 FriendClass 的成员函数为 MyClass 的友元
class MyClass {
private:
    int privateVar;

public:
    friend void FriendClass::accessPrivateVar(MyClass& obj);
};

其他

  • 想要把一组重载函数声明为友元,需要对这组函数中的每一个分别声明