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

C++类成员访问属性的详细解析

2023-09-176.7k 阅读

C++ 类成员访问属性概述

在 C++ 编程中,类是一种强大的用户自定义数据类型,它允许将数据和函数封装在一起。类成员访问属性决定了类的成员(数据成员和成员函数)在类外部以及派生类中的可访问性。C++ 提供了三种主要的访问修饰符:publicprivateprotected。这些修饰符极大地增强了代码的安全性和模块化,使得我们能够更好地控制类的使用方式。

public 访问修饰符

public 修饰的成员在类的外部可以直接访问。这意味着任何代码,无论是类的成员函数、外部函数还是其他类的成员函数,都可以访问 public 成员。

以下是一个简单的示例:

#include <iostream>

class Rectangle {
public:
    // 公有数据成员
    int width;
    int height;

    // 公有成员函数
    int getArea() {
        return width * height;
    }
};

int main() {
    Rectangle rect;
    rect.width = 5;
    rect.height = 3;
    std::cout << "Rectangle area: " << rect.getArea() << std::endl;
    return 0;
}

在上述代码中,widthheightgetArea 函数都是 public 的,因此在 main 函数中可以直接访问 widthheight 来设置矩形的尺寸,并调用 getArea 函数计算面积。

private 访问修饰符

private 修饰的成员只能在类的内部被访问,即只有类的成员函数可以访问 private 成员。外部代码,包括其他类的成员函数,都无法直接访问 private 成员。这有助于隐藏类的内部实现细节,保护数据不被外部非法修改。

#include <iostream>

class Circle {
private:
    // 私有数据成员
    double radius;

public:
    // 公有成员函数,用于设置半径
    void setRadius(double r) {
        if (r >= 0) {
            radius = r;
        } else {
            std::cerr << "Invalid radius value." << std::endl;
        }
    }

    // 公有成员函数,用于获取面积
    double getArea() {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle circle;
    circle.setRadius(5.0);
    std::cout << "Circle area: " << circle.getArea() << std::endl;
    // 以下代码会报错,因为radius是private的
    // circle.radius = -1; 
    return 0;
}

在这个 Circle 类的例子中,radiusprivate 数据成员,外部代码不能直接访问它。通过 setRadius 公有成员函数来设置 radius,并且在 setRadius 函数中可以进行参数验证,保证 radius 的值是合法的。

protected 访问修饰符

protected 修饰的成员与 private 成员类似,在类的外部不能直接访问。但是,protected 成员在派生类(子类)中是可以访问的。这在实现继承关系时非常有用,使得基类可以将一些内部数据或函数暴露给派生类,同时对外部代码保持隐藏。

#include <iostream>

class Shape {
protected:
    // 受保护的数据成员
    int x;
    int y;

public:
    Shape(int a, int b) : x(a), y(b) {}
};

class Triangle : public Shape {
public:
    Triangle(int a, int b) : Shape(a, b) {}

    int getArea() {
        return x * y / 2;
    }
};

int main() {
    Triangle tri(4, 5);
    std::cout << "Triangle area: " << tri.getArea() << std::endl;
    // 以下代码会报错,因为x和y是protected的,在外部不能直接访问
    // std::cout << tri.x << std::endl; 
    return 0;
}

在这个例子中,Shape 类的 xyprotected 成员。Triangle 类继承自 Shape 类,在 Triangle 类的 getArea 成员函数中可以访问 xy 来计算三角形的面积。而在外部代码中,xy 是不可访问的。

访问修饰符的默认情况

在 C++ 中,如果在类定义中没有明确指定访问修饰符,那么对于 class 来说,成员的默认访问属性是 private;对于 struct 来说,成员的默认访问属性是 public

class DefaultClass {
    int privateVar; // 默认为private
public:
    int publicVar;
};

struct DefaultStruct {
    int publicVar; // 默认为public
};

这种默认规则在编写代码时需要特别注意,尤其是在从 structclass 转换或者反之的时候,可能会因为访问属性的改变而导致代码行为的变化。

访问修饰符与继承

当一个类从另一个类继承时,基类的访问修饰符会影响派生类对基类成员的访问权限,同时也会影响派生类对象在外部代码中的可访问性。继承方式有三种:public 继承、private 继承和 protected 继承。

public 继承

public 继承中,基类的 public 成员在派生类中仍然是 public,基类的 protected 成员在派生类中仍然是 protected,基类的 private 成员在派生类中仍然不可访问。

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class PublicDerived : public Base {
public:
    void accessMembers() {
        publicVar = 10;
        protectedVar = 20;
        // 以下代码会报错,privateVar在派生类中不可访问
        // privateVar = 30; 
    }
};

int main() {
    PublicDerived pd;
    pd.publicVar = 5;
    // 以下代码会报错,protectedVar在外部不可访问
    // pd.protectedVar = 10; 
    return 0;
}

private 继承

private 继承中,基类的 publicprotected 成员在派生类中都变为 private 成员,基类的 private 成员在派生类中仍然不可访问。这意味着派生类对象在外部代码中无法访问从基类继承的任何成员(除了通过派生类自身的 public 成员函数间接访问)。

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class PrivateDerived : private Base {
public:
    void accessMembers() {
        publicVar = 10;
        protectedVar = 20;
        // 以下代码会报错,privateVar在派生类中不可访问
        // privateVar = 30; 
    }
};

int main() {
    PrivateDerived pd;
    // 以下代码会报错,publicVar在外部不可访问,因为是private继承
    // pd.publicVar = 5; 
    return 0;
}

protected 继承

protected 继承中,基类的 public 成员在派生类中变为 protected 成员,基类的 protected 成员在派生类中仍然是 protected,基类的 private 成员在派生类中仍然不可访问。这意味着派生类对象在外部代码中无法访问从基类继承的 publicprotected 成员(除了通过派生类自身的 public 成员函数间接访问),但派生类的派生类(孙子类)可以访问这些成员。

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

class ProtectedDerived : protected Base {
public:
    void accessMembers() {
        publicVar = 10;
        protectedVar = 20;
        // 以下代码会报错,privateVar在派生类中不可访问
        // privateVar = 30; 
    }
};

class GrandChild : public ProtectedDerived {
public:
    void accessGrandParentMembers() {
        publicVar = 30;
        protectedVar = 40;
    }
};

int main() {
    GrandChild gc;
    // 以下代码会报错,publicVar在外部不可访问
    // gc.publicVar = 5; 
    return 0;
}

友元函数与友元类

有时候,我们可能需要让某个函数或类访问另一个类的 privateprotected 成员。在这种情况下,可以使用友元函数或友元类。

友元函数

友元函数是在类定义中使用 friend 关键字声明的非成员函数,它可以访问该类的 privateprotected 成员。

#include <iostream>

class Point {
private:
    int x;
    int y;

public:
    Point(int a, int b) : x(a), y(b) {}

    // 声明友元函数
    friend double distance(Point p1, Point p2);
};

// 友元函数的定义
double distance(Point p1, Point p2) {
    return std::sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
}

int main() {
    Point p1(1, 1);
    Point p2(4, 5);
    std::cout << "Distance between points: " << distance(p1, p2) << std::endl;
    return 0;
}

在这个例子中,distance 函数是 Point 类的友元函数,因此它可以访问 Point 类的 private 成员 xy 来计算两点之间的距离。

友元类

友元类是在另一个类的定义中使用 friend 关键字声明的类,友元类的所有成员函数都可以访问该类的 privateprotected 成员。

#include <iostream>

class Rectangle; // 前向声明

class AreaCalculator {
public:
    double calculateArea(Rectangle rect);
};

class Rectangle {
private:
    int width;
    int height;

public:
    Rectangle(int a, int b) : width(a), height(b) {}

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

double AreaCalculator::calculateArea(Rectangle rect) {
    return rect.width * rect.height;
}

int main() {
    Rectangle rect(5, 3);
    AreaCalculator ac;
    std::cout << "Rectangle area: " << ac.calculateArea(rect) << std::endl;
    return 0;
}

在这个例子中,AreaCalculator 类是 Rectangle 类的友元类,所以 AreaCalculator 类的 calculateArea 成员函数可以访问 Rectangle 类的 private 成员 widthheight 来计算矩形的面积。

访问控制与多态性

在 C++ 中,多态性通过虚函数和指针或引用实现。访问修饰符在多态性的场景下也起着重要作用。

虚函数的访问控制

虚函数在基类和派生类中的访问控制需要保持一致。例如,如果基类中的虚函数是 public,那么派生类中重写的虚函数也必须是 public。否则,在通过基类指针或引用调用虚函数时,可能会因为访问权限问题导致编译错误。

class Base {
public:
    virtual void print() {
        std::cout << "Base class print." << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived class print." << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->print();
    delete basePtr;
    return 0;
}

在这个例子中,Base 类的 print 虚函数是 publicDerived 类重写的 print 函数也是 public,这样通过 Base 指针调用 print 函数时就不会出现访问权限问题。

访问控制与动态绑定

动态绑定是指在运行时根据对象的实际类型来决定调用哪个虚函数。访问修饰符会影响动态绑定的过程。如果通过基类指针或引用调用虚函数,编译器会检查指针或引用类型的访问权限,而不是对象实际类型的访问权限。

class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};

class Derived : public Base {
private:
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    // 以下代码会报错,因为Derived类的display函数是private
    // basePtr->display(); 
    delete basePtr;
    return 0;
}

在这个例子中,虽然 Derived 类重写了 display 函数,但由于它是 private 的,通过 Base 指针调用 display 函数时会因为访问权限问题而报错,尽管实际对象类型是 Derived

访问修饰符在模板中的应用

模板是 C++ 中强大的泛型编程工具,访问修饰符在模板类和模板函数中同样适用。

模板类的访问修饰符

模板类可以像普通类一样使用访问修饰符来控制成员的访问权限。

template <typename T>
class Stack {
private:
    T data[100];
    int top;

public:
    Stack() : top(-1) {}

    void push(T value) {
        if (top < 99) {
            data[++top] = value;
        }
    }

    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        return T();
    }
};

int main() {
    Stack<int> intStack;
    intStack.push(10);
    std::cout << "Popped value: " << intStack.pop() << std::endl;
    return 0;
}

在这个 Stack 模板类中,datatopprivate 成员,pushpoppublic 成员函数,控制了对栈数据的访问。

模板函数与访问修饰符

模板函数也可以作为类的成员函数,其访问权限由类的访问修饰符决定。

class Container {
private:
    int arr[10];

public:
    template <typename T>
    void fill(T value) {
        for (int i = 0; i < 10; ++i) {
            arr[i] = static_cast<int>(value);
        }
    }

    void print() {
        for (int i = 0; i < 10; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    Container cont;
    cont.fill(5);
    cont.print();
    return 0;
}

在这个 Container 类中,fill 模板函数是 public 成员函数,因此可以在外部代码中调用,用于填充 arr 数组。

访问修饰符的最佳实践

  1. 数据隐藏:尽可能将数据成员设为 privateprotected,通过 public 成员函数来提供对数据的访问和修改,这样可以更好地控制数据的一致性和安全性。
  2. 继承关系:在设计继承体系时,根据派生类对基类成员的访问需求选择合适的继承方式。如果希望派生类能够扩展和重用基类的功能,同时保持外部接口的一致性,通常使用 public 继承;如果只想在派生类内部使用基类的功能,不希望外部访问,可以使用 privateprotected 继承。
  3. 友元的使用:谨慎使用友元函数和友元类,因为它们破坏了类的封装性。只有在确实需要让外部函数或类访问类的内部成员,且没有其他更好的解决方案时才使用友元。
  4. 代码可读性:清晰地使用访问修饰符,使代码结构更清晰,让其他开发者能够快速理解类的接口和内部实现的访问规则。

通过合理运用 C++ 的类成员访问属性,可以编写出更健壮、安全且易于维护的代码。不同的访问修饰符在不同的场景下各有其用途,深入理解并熟练运用它们是成为优秀 C++ 开发者的关键之一。无论是小型项目还是大型软件系统,正确的访问控制都能为代码的可扩展性和稳定性提供有力保障。在实际编程中,应根据具体需求和设计原则,仔细权衡各种访问属性的使用,以实现最佳的编程效果。同时,随着项目的演进和功能的增加,对访问控制的管理和调整也需要持续关注,确保代码的整体质量和可维护性。例如,在开发一个大型的图形渲染引擎时,不同模块之间的类可能存在复杂的继承关系和数据交互,合理设置访问修饰符可以有效地避免模块之间的非法访问,提高系统的稳定性和安全性。又如,在开发一个数据库访问层库时,通过严格的数据隐藏和合理的接口暴露,可以让其他模块安全地使用数据库功能,而不用担心数据被意外修改。总之,C++ 类成员访问属性是 C++ 编程中至关重要的一部分,深入掌握它对于开发高质量的软件至关重要。