C++ 面向对象_3

C++ 访问限定符与友元

访问限定符

访问限定符(Access Specifiers)是 C++ 中用于控制类成员(包括成员变量和成员函数)访问权限的关键字。它们定义了类内部成员对外部代码的可见性和可访问性,主要用于实现封装(Encapsulation),这是面向对象编程的核心原则之一。C++ 中有三种主要的访问限定符:

  1. public:公共访问权限,类内的成员可以被类外部的任何代码访问。
  2. private:私有访问权限,只有类内部的成员函数或友元(friend)可以访问,外部代码无法直接访问。
  3. protected:受保护访问权限,类似于 private,但类的派生类(子类)可以通过继承访问这些成员。

默认情况下,如果不显式指定访问限定符,类的成员默认是 private(对于 class 关键字定义的类);而对于 struct 定义的结构体,默认是 public。

为什么需要访问限定符?

访问限定符的存在帮助 C++ 实现面向对象的封装性,通过限制对类内部成员的直接访问,访问限定符保护了数据的完整性,避免外部代码随意修改类成员导致的不可预期的行为。例如,一个银行账户类可能有余额(balance)成员变量,如果没有 private 限制,外部代码可能直接将余额设置为负数。

public 成员定义了类的外部接口,外部代码只能通过这些接口与类进行交互,而 private 成员则隐藏了实现的细节,这种接口和实现分离使得开发者可以在不影响外部代码的情况下修改类的内部实现。例如,引入类 A 在没有访问限定符的情况下,随意使用类 A 的成员,如果类 A 进行了内部的修改,就会使其他程序受到影响,因此,可行的做法是只访问类 A 暴露在外部的成员。

同时出于安全性与控制的考虑,访问限定符提供了一种权限控制机制,确保只有经过授权的代码(例如类自身或其派生类)能够访问特定成员,从而提高代码的安全性。

如何使用访问限定符?

访问限定符通常在类的定义中使用,放在成员声明之前。

#include <iostream>
using namespace std;

class MyClass {
public:    // 公共成员
    int publicVar = 10;
    void publicMethod() {
        cout << "Public method called" << endl;
    }

private:   // 私有成员
    int privateVar = 20;
    void privateMethod() {
        cout << "Private method called" << endl;
    }

protected: // 受保护成员
    int protectedVar = 30;
    void protectedMethod() {
        cout << "Protected method called" << endl;
    }

public:    // 可以多次使用访问限定符
    void accessPrivate() {
        // 类内部可以访问私有成员
        cout << "Accessing privateVar: " << privateVar << endl;
        privateMethod();
    }
};

class DerivedClass : public MyClass {
public:
    void accessProtected() {
        // 派生类可以访问基类的受保护成员
        cout << "Accessing protectedVar: " << protectedVar << endl;
        protectedMethod();
    }
};

int main() {
    MyClass obj;
    cout << obj.publicVar << endl;    // 可以访问
    obj.publicMethod();               // 可以调用
    // cout << obj.privateVar << endl;   // 错误:privateVar 不可访问
    // obj.privateMethod();             // 错误:privateMethod 不可访问
    // cout << obj.protectedVar << endl; // 错误:protectedVar 不可访问

    obj.accessPrivate();              // 通过公共方法间接访问私有成员

    DerivedClass derivedObj;
    derivedObj.accessProtected();     // 通过派生类访问受保护成员

    return 0;
}

在上面的例子中,publicVar 和 publicMethod 可以被外部直接访问。privateVar 和 privateMethod 只能在类内部访问,外部无法直接调用。protectedVar 和 protectedMethod 可以被派生类访问,但外部仍然无法直接访问。


访问限定符的作用是在编译时由编译器强制执行的逻辑约束,而不是运行时的内存保护机制。访问限定符的限制是在编译阶段完成的,如果编译器在编译时检查到代码试图访问不允许的成员,编译器就会报错。例如,obj.privateVar 会触发编译错误。

友元

友元(friend)是一种机制,允许特定的类或函数访问另一个类的私有(private)和保护(protected)成员。正常情况下,类的私有和受保护成员只能由该类的成员函数或继承的子类(对于受保护成员 protected)访问。但通过友元机制,可以打破这种封装限制,将访问权限授予指定的外部函数或类。

友元包括:

  1. 友元函数:一个普通的全局函数或另一个类的成员函数。
  2. 友元类:整个类的所有成员函数都可以访问目标类的私有和受保护成员。

为什么需要友元?

友元机制的存在是为了在某些特定场景下提供更高的灵活性,同时不破坏封装性,当两个类需要紧密协作,且一个类需要访问另一个类的私有数据时,友元可以避免通过公共接口(getter/setter)暴露过多的细节。这样,可以通过直接访问私有成员减少函数调用开销,优化程序性能,例如,一个类可能是另一个类的辅助类(如迭代器类需要访问容器的内部数据结构)友元就能简化程序的设计。

但是友元的使用应该保持谨慎,因为这种访问私有和受保护成员的方式在一定程度上是破坏了类的封装性,可能会导致代码的可维护性和安全性降低。

如何使用友元?

  1. 友元函数

    通过在类中用 friend 关键字声明某个函数,使其成为友元函数。友元函数可以是全局函数或另一个类的成员函数。

    #include <iostream>
    class Box {
    private:
        double width;
    
    public:
        Box(double w) : width(w) {}
        // 声明友元函数
        friend void printWidth(Box b);
    };
    
    // 定义友元函数
    void printWidth(Box b) {
        std::cout << "Width: " << b.width << std::endl; // 直接访问私有成员
    }
    
    int main() {
        Box box(10.5);
        printWidth(box); // 输出: Width: 10.5
        return 0;
    }
    
  2. 友元成员函数(另一个类的成员函数)

    class FriendClass;
    class ClassName {
    private:
        int data;
    public:
        friend void FriendClass::friendMethod(ClassName obj);
    };
    

    由于程序是从上到下进行编译的,需要先给出友元成员函数所属类的声明,后面会引入其他的关于友元成员函数定义方式。

  3. 友元类

    通过在类中声明另一个类为友元类,使该类的所有成员函数都可以访问当前类的私有和受保护成员。

    #include <iostream>
    class Box {
    private:
        double width;
    
    public:
        Box(double w) : width(w) {}
        // 声明友元类
        friend class Printer;
    };
    
    class Printer {
    public:
        void print(Box b) {
            std::cout << "Width: " << b.width << std::endl; // 直接访问私有成员
        }
    };
    
    int main() {
        Box box(10.5);
        Printer p;
        p.print(box); // 输出: Width: 10.5
        return 0;
    }
    

需要注意的是友元关系是单向的,A 是 B 的友元,不意味着 B 是 A 的友元。友元关系不可传递,A 是 B 的友元,B 是 C 的友元,不意味着 A 是 C 的友元。友元声明必须在类定义的内部,且通常放在类的开头或者结尾。


友元机制并没有改变类的内存布局或对象存储方式,类的私有和受保护成员仍然按照类的定义方式存储,因此友元机制也属于编译时逻辑并没有引入运行时开销。
正常情况下,编译器会检查访问权限,如果试图访问私有成员,会报错。当某个函数或类被声明为友元时,编译器会在符号解析阶段允许该函数或类直接操作目标对象的内存地址,绕过访问限制。

在类内首次声明友元类或友元函数

void func();
class FriendClass;

class ClassName {
private:
    int data;
public:
    friend void func();
    friend class FriendClass;
};

前面提过,由于程序是从上到下进行编译的,需要先给出友元函数或者友元类的声明这里不关心友元函数和友元类的定义,在编译时才能识别到标识符信息,友元函数和友元类既然是作为类的友元一定会在内部构造其要访问的类的对象,如果说将友元类或友元函数定义在类的前面,这也与从上到下编译时产生矛盾,因此上面的操作一般是固定的,但是这种操作总是要先声明友元函数或友元类,因此如果是首次在类中声明友元类或友元函数,C++ 默认在类中声明的友元函数或友元类就是函数或类的声明,因此,可以直接省略声明的步骤。

class ClassName {
private:
    int data;
public:
    friend void func();
    friend class FriendClass;
};

注意使用限定名称引入友元并非友元类(友元函数)的声明

如果在类中要首次声明一个友元,那就要在声明友元时画蛇添足地增加作用域限定符

// void func();

class ClassName {
private:
    int data;
public:
    friend void ::func();
};

上面的 ::func() 并不是函数声明,而是告诉编译器全局作用域中有一个 func() 作为友元,除非我们在全局域给出 func() 的声明,否则编译器在编译到友元声明之前没有在全局域识别到 func() 的声明就会报错。

友元函数的类外定义与类内定义

  1. 类外定义:

    在类内用 friend 声明函数,在类外提供实现。

    class ClassName {
        friend 返回类型 函数名(参数列表);
    };
    返回类型 函数名(参数列表) { 函数体 }
    
  2. 类内声明并定义:

友元函数直接在类中声明并提供实现。

class str
{
    int val;

    friend void func()
    {
        std::cout << val << std::endl;
    }
}

这种方式可以参考 The Power of Hidden Friends in C++

提供了全局域中的友元函数的隐藏,即 func() 其实是在全局域但被隐藏,要在类外给出 func() 的声明,这种好处是我提供一个类并提供了友元函数,但我不希望引入的友元函数影响全局域,减轻了编译器的负担,不需要去全局查找友元的定义,也防止误用,不过最好的方式是在类外给出定义。

下面是最佳的隐藏友元使用方式

class HiddenFriendDemo {
private:
    int value;
public:
    HiddenFriendDemo(int v) : value(v) {}
    friend void print(HiddenFriendDemo d) {
        cout << "Hidden friend value: " << d.value << endl;
    }
};


HiddenFriendDemo obj(42);
print(obj);  // 正确:通过 ADL 找到 print
// print(42); // 错误:无法直接调用,因为 print 是隐藏的