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

C++类定义的核心要素剖析

2023-06-122.9k 阅读

C++ 类定义的核心要素剖析

类的基本概念与定义结构

在 C++ 中,类(class)是一种用户自定义的数据类型,它将数据成员(也称为属性)和成员函数(也称为方法)封装在一起,提供了一种将相关数据和操作组合成一个单一实体的方式。这种封装性使得代码的组织更加清晰,易于维护和扩展。

类的定义通常包含以下基本结构:

class ClassName {
    // 访问修饰符
    private:
        // 私有数据成员和成员函数
        int privateData;
        void privateFunction();
    public:
        // 公有数据成员和成员函数
        int publicData;
        void publicFunction();
    protected:
        // 保护数据成员和成员函数
        int protectedData;
        void protectedFunction();
};

在上述代码中,ClassName 是类的名称。类体被大括号 {} 包围,内部通过访问修饰符(privatepublicprotected)来控制对数据成员和成员函数的访问权限。

访问修饰符的深入理解

  1. private 访问修饰符
    • 含义:被 private 修饰的数据成员和成员函数只能在类的内部被访问。这意味着类的外部代码,包括其他函数和其他类,都无法直接访问这些成员。
    • 作用:实现数据隐藏,保护类的内部数据不被外部随意修改,确保数据的完整性和一致性。
    • 示例
class BankAccount {
private:
    double balance;
public:
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount account;
    // account.balance = 1000;  // 错误,balance 是 private 的,无法在类外访问
    account.deposit(500);
    double bal = account.getBalance();
    return 0;
}

在这个 BankAccount 类中,balanceprivate 数据成员,外部代码不能直接修改它,只能通过 deposit 等公有成员函数来间接操作,从而保证了账户余额修改的合法性。

  1. public 访问修饰符
    • 含义:被 public 修饰的数据成员和成员函数可以在类的内部和外部被访问。这是类与外部世界交互的接口。
    • 作用:提供一种机制,让外部代码能够使用类提供的功能,如调用成员函数来操作对象的状态。
    • 示例
class Rectangle {
public:
    int width;
    int height;
    int getArea() {
        return width * height;
    }
};

int main() {
    Rectangle rect;
    rect.width = 5;
    rect.height = 10;
    int area = rect.getArea();
    return 0;
}

Rectangle 类中,widthheightgetArea 函数都是 public 的,外部代码可以直接访问和操作这些成员。

  1. protected 访问修饰符
    • 含义:被 protected 修饰的数据成员和成员函数可以在类的内部以及该类的派生类(子类)中被访问,但不能在类的外部直接访问。
    • 作用:在继承体系中,它提供了一种介于 privatepublic 之间的访问控制级别,允许子类访问父类的某些内部成员,同时防止外部代码的直接访问。
    • 示例
class Shape {
protected:
    int color;
public:
    void setColor(int c) {
        color = c;
    }
};

class Circle : public Shape {
public:
    void printColor() {
        // 可以访问 Shape 类中的 protected 成员 color
        std::cout << "Circle color: " << color << std::endl;
    }
};

int main() {
    Circle circle;
    circle.setColor(1);
    circle.printColor();
    // std::cout << circle.color;  // 错误,color 是 protected 的,不能在类外访问
    return 0;
}

在这个示例中,Shape 类的 color 成员是 protected 的,Circle 类作为 Shape 的子类可以访问 color,但外部代码不能直接访问。

数据成员

  1. 数据成员的类型
    • 基本数据类型:类的数据成员可以是 C++ 的基本数据类型,如 intdoublecharbool 等。例如:
class Point {
private:
    int x;
    int y;
public:
    void setCoordinates(int a, int b) {
        x = a;
        y = b;
    }
    void printCoordinates() {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

Point 类中,xy 是基本数据类型 int 的数据成员,用于表示点在二维平面上的坐标。

  • 自定义数据类型:数据成员也可以是用户自定义的数据类型,包括其他类的对象。例如:
class Date {
private:
    int day;
    int month;
    int year;
public:
    void setDate(int d, int m, int y) {
        day = d;
        month = m;
        year = y;
    }
    void printDate() {
        std::cout << day << "/" << month << "/" << year << std::endl;
    }
};

class Employee {
private:
    std::string name;
    Date hireDate;
public:
    void setEmployeeInfo(const std::string& n, int d, int m, int y) {
        name = n;
        hireDate.setDate(d, m, y);
    }
    void printEmployeeInfo() {
        std::cout << "Name: " << name << ", Hire Date: ";
        hireDate.printDate();
    }
};

Employee 类中,hireDateDate 类的对象,作为 Employee 类的数据成员,用于表示员工的雇佣日期。

  • 指针和引用类型:数据成员还可以是指针或引用类型。例如:
class MyClass {
private:
    int* ptr;
    int& ref;
public:
    MyClass(int& value) : ref(value) {
        ptr = new int(value);
    }
    ~MyClass() {
        delete ptr;
    }
    void printValues() {
        std::cout << "Value by pointer: " << *ptr << ", Value by reference: " << ref << std::endl;
    }
};

int main() {
    int num = 10;
    MyClass obj(num);
    obj.printValues();
    return 0;
}

MyClass 类中,ptr 是指向 int 类型的指针,refint 类型的引用。指针和引用在类中常用于动态内存管理或避免对象拷贝等场景。

  1. 数据成员的初始化
    • 在构造函数中初始化:最常见的数据成员初始化方式是在构造函数中进行。例如:
class Person {
private:
    std::string name;
    int age;
public:
    Person(const std::string& n, int a) {
        name = n;
        age = a;
    }
    void printInfo() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

Person 类的构造函数中,通过赋值语句对 nameage 进行初始化。

  • 使用成员初始化列表:成员初始化列表是一种更高效的数据成员初始化方式,尤其适用于初始化那些需要调用特定构造函数的对象,或者是常量成员和引用成员。例如:
class Student {
private:
    const int id;
    std::string name;
public:
    Student(int i, const std::string& n) : id(i), name(n) {
        // 这里不能再对 id 赋值,因为它是 const
    }
    void printStudentInfo() {
        std::cout << "ID: " << id << ", Name: " << name << std::endl;
    }
};

Student 类中,idconst 类型,必须在成员初始化列表中进行初始化。name 也可以通过成员初始化列表初始化,这种方式避免了先默认构造再赋值的额外开销。

成员函数

  1. 成员函数的定义
    • 在类体中定义:成员函数可以直接在类体中定义,这种情况下函数会被隐式声明为内联函数(如果函数体简单,编译器可能会将其优化为内联)。例如:
class Square {
private:
    int side;
public:
    int getArea() {
        return side * side;
    }
    void setSide(int s) {
        side = s;
    }
};

Square 类中,getAreasetSide 函数都在类体中定义。

  • 在类体外定义:对于较为复杂的成员函数,通常在类体外定义,以提高代码的可读性和可维护性。在类体外定义时,需要使用作用域解析运算符 ::。例如:
class Triangle {
private:
    int base;
    int height;
public:
    void setDimensions(int b, int h);
    double getArea();
};

void Triangle::setDimensions(int b, int h) {
    base = b;
    height = h;
}

double Triangle::getArea() {
    return 0.5 * base * height;
}

在这个 Triangle 类中,setDimensionsgetArea 函数在类体外定义,通过 Triangle:: 明确了函数所属的类。

  1. 成员函数的参数和返回值
    • 参数:成员函数可以接受零个或多个参数,这些参数可以是基本数据类型、自定义数据类型、指针或引用。例如:
class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    Complex add(const Complex& other) {
        Complex result;
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;
    }
};

Complex 类的 add 成员函数中,接受一个 Complex 类对象的引用作为参数,用于实现复数的加法运算。

  • 返回值:成员函数可以返回任何数据类型,包括基本数据类型、自定义数据类型、指针或引用。例如:
class StringHolder {
private:
    std::string str;
public:
    StringHolder(const std::string& s) : str(s) {}
    const std::string& getString() const {
        return str;
    }
};

StringHolder 类中,getString 函数返回一个 const std::string&,即对内部 std::string 对象的常量引用,这样可以避免不必要的对象拷贝。

  1. this 指针
    • 含义:在类的成员函数中,this 是一个隐含的指针,它指向调用该成员函数的对象。通过 this 指针,可以访问对象的数据成员和其他成员函数。
    • 作用this 指针主要用于区分同名的成员变量和局部变量,以及在成员函数返回对象自身的引用时使用。例如:
class Counter {
private:
    int count;
public:
    Counter() : count(0) {}
    Counter& increment() {
        count++;
        return *this;
    }
    int getCount() {
        return count;
    }
};

int main() {
    Counter c;
    c.increment().increment();
    int value = c.getCount();
    return 0;
}

Counter 类的 increment 函数中,返回 *this,这样可以实现链式调用,如 c.increment().increment()。同时,在函数内部,this 指针隐含指向调用 increment 函数的 Counter 对象,从而可以访问 count 数据成员。

  1. 常量成员函数
    • 含义:常量成员函数是指不会修改对象状态的成员函数。在函数声明和定义中,在参数列表后加上 const 关键字来标识。
    • 作用:常量成员函数可以被常量对象调用,这在很多场景下非常有用,例如在实现只读操作时。同时,它也有助于编译器进行优化,因为编译器知道该函数不会修改对象状态。例如:
class Book {
private:
    std::string title;
    std::string author;
public:
    Book(const std::string& t, const std::string& a) : title(t), author(a) {}
    const std::string& getTitle() const {
        return title;
    }
    const std::string& getAuthor() const {
        return author;
    }
};

int main() {
    const Book book("C++ Primer", "Stanley Lippman");
    std::string title = book.getTitle();
    return 0;
}

Book 类中,getTitlegetAuthor 函数都是常量成员函数,它们可以被 const Book 对象调用,以获取书籍的标题和作者信息,而不会修改对象的状态。

构造函数和析构函数

  1. 构造函数
    • 定义和作用:构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的数据成员。构造函数的名称与类名相同,并且没有返回类型(包括 void 也没有)。例如:
class Box {
private:
    double length;
    double width;
    double height;
public:
    Box(double l = 1.0, double w = 1.0, double h = 1.0) : length(l), width(w), height(h) {
        std::cout << "Box constructed." << std::endl;
    }
};

Box 类中,构造函数 Box(double l, double w, double h) 用于初始化 lengthwidthheight 数据成员。如果在创建对象时没有提供参数,会使用默认参数值。

  • 构造函数的重载:一个类可以有多个构造函数,只要它们的参数列表不同,这就是构造函数的重载。例如:
class Circle {
private:
    double radius;
public:
    Circle() : radius(0) {
        std::cout << "Circle with radius 0 constructed." << std::endl;
    }
    Circle(double r) : radius(r) {
        std::cout << "Circle with radius " << r << " constructed." << std::endl;
    }
};

Circle 类中,有两个构造函数,一个无参数构造函数,另一个带一个 double 类型参数的构造函数,分别用于不同的初始化场景。

  • 初始化列表的重要性:如前文所述,使用成员初始化列表初始化数据成员比在构造函数体中赋值更高效,特别是对于自定义类型的数据成员。例如:
class MyString {
private:
    std::string str;
public:
    MyString(const char* s) : str(s) {
        // 这里使用成员初始化列表初始化 str,避免了先默认构造再赋值
    }
};

MyString 类中,使用成员初始化列表初始化 str,如果在构造函数体中赋值,会先默认构造 str,然后再赋值,增加了不必要的开销。

  1. 析构函数
    • 定义和作用:析构函数是另一种特殊的成员函数,它在对象被销毁时自动调用,用于释放对象占用的资源,如动态分配的内存等。析构函数的名称是在类名前加上波浪号 ~,同样没有返回类型。例如:
class DynamicArray {
private:
    int* arr;
    int size;
public:
    DynamicArray(int s) : size(s) {
        arr = new int[size];
    }
    ~DynamicArray() {
        delete[] arr;
        std::cout << "DynamicArray destroyed." << std::endl;
    }
};

DynamicArray 类中,构造函数动态分配了一个整数数组,析构函数在对象销毁时释放该数组占用的内存,避免内存泄漏。

  • 析构函数的调用时机:析构函数在以下几种情况下会被调用:
    • 当对象的生命周期结束时,例如在函数中定义的局部对象,当函数执行完毕时,对象会被销毁,析构函数被调用。
    • 当使用 delete 运算符释放通过 new 运算符动态分配的对象时,析构函数会被调用。
    • 当一个对象作为容器(如 std::vectorstd::list 等)的元素被移除时,其析构函数也会被调用。

静态成员

  1. 静态数据成员
    • 定义和特性:静态数据成员是类的所有对象共享的成员,无论创建了多少个类的对象,静态数据成员只有一份实例。它在类的所有对象之间共享数据,并且不依赖于任何特定的对象。静态数据成员需要在类体外进行初始化。例如:
class Company {
private:
    static int employeeCount;
    std::string name;
public:
    Company(const std::string& n) : name(n) {
        employeeCount++;
    }
    ~Company() {
        employeeCount--;
    }
    static int getEmployeeCount() {
        return employeeCount;
    }
};

// 静态数据成员的初始化
int Company::employeeCount = 0;

int main() {
    Company company1("ABC Inc.");
    Company company2("XYZ Corp.");
    int count = Company::getEmployeeCount();
    return 0;
}

Company 类中,employeeCount 是静态数据成员,用于记录公司的员工数量。每个 Company 对象创建时,employeeCount 增加,销毁时减少。getEmployeeCount 是静态成员函数,用于获取当前的员工数量。

  • 访问方式:静态数据成员可以通过类名加作用域解析运算符 :: 来访问,也可以通过对象来访问,但推荐使用类名访问,因为静态成员不依赖于特定对象。例如:
std::cout << Company::employeeCount << std::endl;  // 通过类名访问
Company company("My Company");
std::cout << company.getEmployeeCount() << std::endl;  // 通过对象访问静态成员函数
  1. 静态成员函数
    • 定义和特性:静态成员函数只能访问静态数据成员和其他静态成员函数,不能访问非静态数据成员,因为非静态数据成员依赖于具体的对象实例,而静态成员函数不与任何特定对象关联。静态成员函数可以通过类名直接调用,不需要创建对象。例如:
class MathUtils {
public:
    static double square(double num) {
        return num * num;
    }
};

int main() {
    double result = MathUtils::square(5);
    return 0;
}

MathUtils 类中,square 是静态成员函数,它可以直接通过类名 MathUtils::square 调用,用于计算一个数的平方。

友元

  1. 友元函数
    • 定义和作用:友元函数是一种非成员函数,但它可以访问类的私有和保护成员。通过将一个函数声明为类的友元,类给予了该函数特殊的访问权限。友元函数的声明通常放在类体中,使用 friend 关键字。例如:
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) {
    int dx = p1.x - p2.x;
    int dy = p1.y - p2.y;
    return std::sqrt(dx * dx + dy * dy);
}

Point 类中,distance 函数被声明为友元函数,这样它就可以访问 Point 类的私有成员 xy,用于计算两个点之间的距离。

  • 注意事项:虽然友元函数提供了灵活的访问方式,但过度使用会破坏类的封装性,因此应谨慎使用。
  1. 友元类
    • 定义和作用:友元类是指一个类的所有成员函数都可以访问另一个类的私有和保护成员。同样通过 friend 关键字声明。例如:
class Engine {
private:
    int power;
public:
    Engine(int p) : power(p) {}
    friend class Car;
};

class Car {
private:
    std::string model;
    Engine engine;
public:
    Car(const std::string& m, int p) : model(m), engine(p) {}
    void displayInfo() {
        std::cout << "Model: " << model << ", Engine Power: " << engine.power << std::endl;
    }
};

在这个例子中,Car 类是 Engine 类的友元类,所以 Car 类的成员函数 displayInfo 可以访问 Engine 类的私有成员 power

总结

C++ 类定义的这些核心要素,包括访问修饰符、数据成员、成员函数、构造函数、析构函数、静态成员和友元等,相互配合,构成了 C++ 面向对象编程的基础。理解和熟练运用这些要素,能够帮助开发者编写出结构清晰、可维护性强、安全性高的代码。在实际编程中,需要根据具体的需求和设计目标,合理地使用这些要素,以实现高效、可靠的软件系统。通过不断地实践和学习,开发者可以更加深入地掌握 C++ 类的特性,充分发挥 C++ 语言在大型项目开发中的优势。同时,随着 C++ 标准的不断演进,一些新的特性和改进也可能会影响类的定义和使用方式,开发者需要持续关注并学习,以保持代码的先进性和兼容性。