MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 访问控制深入解析

2021-09-107.8k 阅读

C++ 访问控制基础概念

在 C++ 编程中,访问控制是一项至关重要的机制,它用于限制对类的成员(包括数据成员和成员函数)的访问。通过合理设置访问控制,我们能够确保数据的安全性和一致性,同时也有助于实现面向对象编程中的封装和信息隐藏原则。

访问修饰符概述

C++ 提供了三种主要的访问修饰符:publicprivateprotected。这些修饰符决定了类成员在类的外部以及派生类中的可访问性。

  • public(公有):声明为 public 的成员可以在类的外部被直接访问。这意味着任何函数,无论是类的成员函数还是普通的全局函数,都可以访问 public 成员。通常,类的接口部分(例如供外部调用的成员函数)会被声明为 public,以便其他代码能够与该类进行交互。
  • private(私有)private 成员只能在类的内部被访问,即只有类的成员函数和友元函数(稍后会详细介绍)可以操作这些成员。private 成员主要用于存储类的内部数据,将其隐藏起来,防止外部代码直接修改,从而保证数据的完整性和安全性。
  • protected(保护)protected 成员的访问权限介于 publicprivate 之间。它们可以在类的内部以及派生类中被访问,但不能在类的外部直接访问。这对于那些希望在派生类中使用,但又不想暴露给外部代码的成员来说非常有用。

访问控制在类定义中的应用

下面通过一个简单的类定义示例来展示这三种访问修饰符的使用。

class MyClass {
public:
    // 公有成员函数,可在类外部调用
    void publicFunction() {
        std::cout << "This is a public function." << std::endl;
    }

private:
    // 私有数据成员,只能在类内部访问
    int privateData;

protected:
    // 保护成员函数,可在类内部及派生类中访问
    void protectedFunction() {
        std::cout << "This is a protected function." << std::endl;
    }
};

在上述示例中,publicFunctionpublic 成员函数,任何外部代码都可以调用它。而 privateDataprivate 数据成员,外部代码无法直接访问。protectedFunctionprotected 成员函数,它可以在 MyClass 类的内部以及从 MyClass 派生的类中被调用。

类外部访问 public 成员

要在类的外部访问 public 成员,我们只需要创建类的对象,然后使用对象名和成员访问运算符(.->)来调用 public 成员函数或访问 public 数据成员(如果有的话)。

#include <iostream>

int main() {
    MyClass obj;
    obj.publicFunction(); // 合法,调用公有成员函数
    // obj.privateData;   // 非法,无法访问私有数据成员
    // obj.protectedFunction(); // 非法,无法访问保护成员函数
    return 0;
}

类内部访问成员

在类的成员函数内部,所有成员(包括 publicprivateprotected)都可以被访问。这是因为成员函数是类的一部分,它有权限操作类的所有数据和函数。

class AnotherClass {
public:
    void accessAllMembers() {
        publicData = 10;
        privateData = 20;
        protectedFunction();
    }

public:
    int publicData;

private:
    int privateData;

protected:
    void protectedFunction() {}
};

accessAllMembers 函数中,我们可以看到它能够自由地访问 publicData(公有数据成员)、privateData(私有数据成员)以及调用 protectedFunction(保护成员函数)。

继承与访问控制

继承是 C++ 面向对象编程的重要特性之一,它允许一个类(派生类)从另一个类(基类)获取成员。在继承关系中,访问控制规则变得更加复杂,因为我们需要考虑基类成员在派生类中的访问权限变化。

继承方式对访问权限的影响

C++ 支持三种继承方式:public 继承、private 继承和 protected 继承。不同的继承方式会改变基类成员在派生类中的访问权限。

  • public 继承:在 public 继承中,基类的 public 成员在派生类中仍然是 public,基类的 protected 成员在派生类中仍然是 protected,而基类的 private 成员在派生类中仍然是不可访问的(即使通过成员函数也不行)。
class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class PublicDerived : public Base {
public:
    void accessBaseMembers() {
        publicData = 10; // 合法,public 成员在派生类中仍然是 public
        protectedData = 20; // 合法,protected 成员在派生类中仍然是 protected
        // privateData = 30; // 非法,private 成员在派生类中不可访问
    }
};
  • private 继承:在 private 继承中,基类的 publicprotected 成员在派生类中都变成 private。基类的 private 成员在派生类中仍然不可访问。
class PrivateDerived : private Base {
public:
    void accessBaseMembers() {
        publicData = 10; // 合法,但变为 private 成员,只能在派生类内部访问
        protectedData = 20; // 合法,但变为 private 成员,只能在派生类内部访问
        // privateData = 30; // 非法,private 成员在派生类中不可访问
    }
};
  • protected 继承:在 protected 继承中,基类的 public 成员在派生类中变成 protected,基类的 protected 成员在派生类中仍然是 protected,基类的 private 成员在派生类中不可访问。
class ProtectedDerived : protected Base {
public:
    void accessBaseMembers() {
        publicData = 10; // 合法,变为 protected 成员,只能在派生类及派生类的派生类中访问
        protectedData = 20; // 合法,仍然是 protected 成员
        // privateData = 30; // 非法,private 成员在派生类中不可访问
    }
};

派生类对基类成员的访问权限总结

继承方式/基类成员publicprotectedprivate
public 继承publicprotected不可访问
private 继承privateprivate不可访问
protected 继承protectedprotected不可访问

多层继承中的访问控制

当存在多层继承时,访问控制规则会层层传递。例如,如果有一个类 GrandBase,一个类 BaseGrandBase 继承,然后一个类 DerivedBase 继承,那么 GrandBase 的成员在 Derived 中的访问权限取决于 BaseGrandBase 的继承方式以及 DerivedBase 的继承方式。

class GrandBase {
public:
    int grandPublicData;
protected:
    int grandProtectedData;
private:
    int grandPrivateData;
};

class Base : public GrandBase {
public:
    void accessGrandBaseMembers() {
        grandPublicData = 10; // 合法,public 继承,public 成员仍然是 public
        grandProtectedData = 20; // 合法,public 继承,protected 成员仍然是 protected
        // grandPrivateData = 30; // 非法,private 成员不可访问
    }
};

class Derived : public Base {
public:
    void accessGrandBaseMembersThroughBase() {
        grandPublicData = 100; // 合法,因为 Base 是 public 继承 GrandBase,这里仍然是 public
        grandProtectedData = 200; // 合法,因为 Base 是 public 继承 GrandBase,这里仍然是 protected
        // grandPrivateData = 300; // 非法,private 成员不可访问
    }
};

友元函数与访问控制

友元函数是一种特殊的函数,它虽然不是类的成员函数,但却可以访问类的私有和保护成员。友元函数为我们提供了一种在特定情况下打破访问控制限制的手段。

友元函数的声明

要声明一个友元函数,我们需要在类的定义中使用 friend 关键字。友元函数的声明可以放在类的任何访问修饰符部分(publicprivateprotected),因为它的访问权限并不受所在位置的影响。

class MyFriendClass {
private:
    int privateData;

public:
    // 声明友元函数
    friend void friendFunction(MyFriendClass& obj);

    MyFriendClass(int data) : privateData(data) {}
};

// 友元函数的定义
void friendFunction(MyFriendClass& obj) {
    std::cout << "Accessing private data: " << obj.privateData << std::endl;
}

在上述示例中,friendFunction 被声明为 MyFriendClass 的友元函数,因此它可以访问 MyFriendClass 的私有成员 privateData

友元函数的特点

  • 非成员函数:友元函数不是类的成员函数,它没有 this 指针。这意味着它不能通过对象名直接访问类的成员,而是需要将对象作为参数传递进来。
  • 访问权限:友元函数可以访问类的私有和保护成员,这打破了类的封装性。因此,在使用友元函数时需要谨慎,确保只有在必要的情况下才赋予其这种特殊权限。
  • 位置无关性:如前所述,友元函数的声明位置不影响其访问权限。它可以在 publicprivateprotected 部分声明,效果是一样的。

友元函数与成员函数的区别

成员函数是类的一部分,它可以隐式地访问类的所有成员,并且有 this 指针指向调用该函数的对象。而友元函数是独立于类的函数,需要通过传递对象参数来访问类的成员,并且没有 this 指针。

class ComparisonClass {
private:
    int value;

public:
    ComparisonClass(int val) : value(val) {}

    // 成员函数
    bool isEqualTo(ComparisonClass& other) {
        return this->value == other.value;
    }

    // 友元函数声明
    friend bool isGreaterThan(ComparisonClass& a, ComparisonClass& b);
};

// 友元函数定义
bool isGreaterThan(ComparisonClass& a, ComparisonClass& b) {
    return a.value > b.value;
}

在这个示例中,isEqualTo 是成员函数,它使用 this 指针来访问当前对象的 value。而 isGreaterThan 是友元函数,它通过传递两个对象参数来比较它们的 value

友元类与访问控制

除了友元函数,C++ 还支持友元类。一个类可以被声明为另一个类的友元类,这样友元类的所有成员函数都可以访问被友元类的私有和保护成员。

友元类的声明

声明一个友元类同样使用 friend 关键字。

class FriendClass; // 前向声明

class MyFriendedClass {
private:
    int privateData;

public:
    MyFriendedClass(int data) : privateData(data) {}

    // 声明 FriendClass 为友元类
    friend class FriendClass;
};

class FriendClass {
public:
    void accessPrivateData(MyFriendedClass& obj) {
        std::cout << "Accessing private data from friend class: " << obj.privateData << std::endl;
    }
};

在上述代码中,FriendClass 被声明为 MyFriendedClass 的友元类,因此 FriendClass 的成员函数 accessPrivateData 可以访问 MyFriendedClass 的私有成员 privateData

友元类的传递性与继承性

  • 传递性:友元关系是不具有传递性的。例如,如果 AB 的友元类,BC 的友元类,这并不意味着 AC 的友元类。每个友元关系都需要单独声明。
  • 继承性:友元关系也不具有继承性。如果 AB 的友元类,CB 派生,这并不意味着 AC 的友元类,除非显式声明。

慎用友元类

虽然友元类提供了一种强大的访问控制突破机制,但过度使用友元类会破坏类的封装性和信息隐藏原则。因此,在使用友元类时,应该确保有充分的理由,并且仔细权衡其对代码结构和安全性的影响。

访问控制与模板

模板是 C++ 中一种强大的代码复用机制,它允许我们编写通用的代码,适用于不同的数据类型。当模板与访问控制结合时,会产生一些有趣的情况。

模板类中的访问控制

在模板类中,访问控制规则与普通类相同。我们可以使用 publicprivateprotected 修饰符来控制模板类成员的访问权限。

template <typename T>
class TemplateClass {
private:
    T privateData;

public:
    TemplateClass(T data) : privateData(data) {}

    T getPrivateData() {
        return privateData;
    }
};

在这个模板类 TemplateClass 中,privateData 是私有数据成员,只有成员函数 getPrivateData 可以访问它。

模板函数与访问控制

模板函数同样可以作为友元函数,访问模板类的私有和保护成员。

template <typename T>
class TemplateFriendedClass {
private:
    T privateData;

public:
    TemplateFriendedClass(T data) : privateData(data) {}

    // 声明模板友元函数
    template <typename U>
    friend void templateFriendFunction(TemplateFriendedClass<U>& obj);
};

template <typename T>
void templateFriendFunction(TemplateFriendedClass<T>& obj) {
    std::cout << "Accessing private data in template friend function: " << obj.privateData << std::endl;
}

在上述代码中,templateFriendFunction 是一个模板友元函数,它可以访问 TemplateFriendedClass 的私有成员 privateData

模板特化与访问控制

当进行模板特化时,访问控制规则仍然适用。例如,我们可以为特定类型的模板特化定义不同的访问控制。

template <typename T>
class TemplateWithSpecialization {
private:
    T privateData;

public:
    TemplateWithSpecialization(T data) : privateData(data) {}
};

// 模板特化
template <>
class TemplateWithSpecialization<int> {
public:
    int publicData;
};

在这个例子中,对于 TemplateWithSpecialization<int> 的特化版本,我们将数据成员定义为 public,这与一般模板类的访问控制有所不同。

访问控制在实际编程中的应用场景

访问控制在实际编程中有许多重要的应用场景,它不仅有助于提高代码的安全性和可维护性,还能更好地实现面向对象编程的原则。

数据封装与保护

通过将数据成员声明为 privateprotected,我们可以将数据封装在类的内部,防止外部代码直接修改。只有通过类提供的 public 接口(成员函数)来访问和修改数据,这样可以确保数据的一致性和完整性。

class BankAccount {
private:
    double balance;

public:
    BankAccount(double initialBalance) : balance(initialBalance) {}

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }

    double getBalance() {
        return balance;
    }
};

BankAccount 类中,balance 是私有数据成员,外部代码只能通过 depositwithdrawgetBalance 等公有成员函数来操作它,从而保证了账户余额的合理性。

实现信息隐藏

信息隐藏是面向对象编程的重要原则之一,访问控制是实现信息隐藏的关键手段。通过将类的内部实现细节隐藏起来,只暴露必要的接口给外部代码,我们可以降低代码的耦合度,使得类的内部实现可以独立变化而不影响外部使用。

例如,一个图形绘制库中的 Circle 类可能有一些用于计算圆的面积、周长等的内部算法和数据结构,这些细节对于使用 Circle 类的外部代码来说并不重要。通过将这些实现细节声明为 privateprotected,我们只提供简单的 public 接口,如 drawgetArea 等,实现了信息隐藏。

控制派生类的访问权限

在继承关系中,通过合理设置基类成员的访问权限以及选择合适的继承方式,我们可以精确控制派生类对基类成员的访问。这有助于实现代码的层次结构和功能扩展,同时保证基类的内部实现不被随意篡改。

比如,一个游戏开发框架中,有一个 GameObject 基类,其中一些成员(如对象的位置、速度等)可能需要在派生类(如 PlayerEnemy 等)中被访问和修改,而另一些成员(如内部的渲染优化算法)则不希望被派生类直接访问。通过将前者声明为 protected,后者声明为 private,可以满足这种需求。

与其他设计模式结合

访问控制在许多设计模式中也起着重要作用。例如,在单例模式中,为了确保全局只有一个实例,我们通常将构造函数声明为 private,防止外部代码直接创建实例。只有通过一个 public 的静态成员函数来获取唯一的实例。

class Singleton {
private:
    static Singleton* instance;
    Singleton() {} // 私有构造函数

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

在这个单例模式的实现中,私有构造函数保证了外部代码无法直接创建 Singleton 的实例,只能通过 getInstance 函数来获取单例对象。

访问控制的常见问题与解决方法

在使用访问控制的过程中,开发者可能会遇到一些常见问题,下面我们来分析这些问题并提供相应的解决方法。

意外访问私有成员

有时候,由于代码结构复杂或者疏忽,可能会在类的外部意外尝试访问私有成员,导致编译错误。

问题示例

class PrivateAccessError {
private:
    int privateValue;

public:
    PrivateAccessError(int val) : privateValue(val) {}
};

int main() {
    PrivateAccessError obj(10);
    // int value = obj.privateValue; // 编译错误,尝试访问私有成员
    return 0;
}

解决方法:仔细检查代码,确保只通过类提供的 public 接口来访问对象的成员。如果确实需要访问私有成员,考虑是否可以通过添加合适的 public 成员函数来实现需求。

友元函数和友元类滥用

过度使用友元函数和友元类会破坏类的封装性,增加代码的维护难度。

问题示例

class OverusedFriend {
private:
    int privateData;

public:
    OverusedFriend(int data) : privateData(data) {}

    // 过多的友元函数和友元类声明
    friend void friendFunction1(OverusedFriend& obj);
    friend void friendFunction2(OverusedFriend& obj);
    friend class FriendClass1;
    friend class FriendClass2;
};

解决方法:重新审视友元关系,只在必要的情况下使用友元函数和友元类。尽量通过合理设计类的接口来满足功能需求,而不是依赖过多的友元关系。

继承中的访问权限混乱

在多层继承中,由于继承方式和基类成员访问权限的组合,可能会导致访问权限混乱,难以理解和维护。

问题示例

class Base1 {
public:
    int publicData;
};

class Base2 : private Base1 {
public:
    void accessBase1Data() {
        publicData = 10; // 合法,但变为 private 成员
    }
};

class Derived : public Base2 {
public:
    // 试图访问 Base1 的 publicData,但由于 Base2 是 private 继承,这里不可访问
    // int getBase1Data() { return publicData; } // 编译错误
};

解决方法:在设计继承结构时,仔细规划每个类的继承方式和成员的访问权限。可以通过添加注释或文档来清晰说明每个类成员的访问权限以及继承关系对其的影响。同时,尽量保持继承层次的简洁,避免过度复杂的多层继承。

总结

访问控制是 C++ 编程语言中一项核心特性,它通过 publicprivateprotected 等访问修饰符,以及友元函数和友元类等机制,为我们提供了强大的手段来保护数据、实现封装和信息隐藏。在继承关系中,不同的继承方式进一步影响着基类成员在派生类中的访问权限。

在实际编程中,合理运用访问控制可以提高代码的安全性、可维护性和可扩展性。同时,我们也要注意避免访问控制相关的常见问题,如意外访问私有成员、友元滥用和继承中的访问权限混乱等。通过深入理解和熟练运用访问控制机制,我们能够编写出更加健壮、高效且符合面向对象编程原则的 C++ 代码。无论是开发小型项目还是大型系统,访问控制始终是我们构建可靠软件的重要基石之一。