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

C++类的定义与使用

2024-04-152.6k 阅读

C++ 类的定义

在 C++ 中,类(class)是一种用户自定义的数据类型,它将数据成员(也称为属性)和成员函数(也称为方法)封装在一起,从而实现数据的隐藏和抽象。通过类,我们可以创建对象,每个对象都拥有类中定义的数据成员的副本,并且可以调用类的成员函数。

类定义的基本语法

类的定义通常由关键字 class 开始,后跟类名,然后是一对花括号 {},在花括号内定义类的成员。类的定义以分号 ; 结束。以下是一个简单的类定义示例:

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

    // 成员函数
    int area() {
        return width * height;
    }
};

在上述代码中:

  • class 是定义类的关键字。
  • Rectangle 是类名,遵循 C++ 的命名规则,通常首字母大写。
  • 花括号内定义了类的成员。其中,widthheight 是数据成员,它们存储了矩形的宽度和高度信息。area 是一个成员函数,用于计算矩形的面积。
  • public 是访问说明符,它指定了后续成员(数据成员和成员函数)的访问级别。这里,public 表示这些成员可以在类外部被访问。

访问说明符

C++ 中有三种主要的访问说明符:publicprivateprotected。它们用于控制类成员在类外部的访问权限。

  1. public(公共的):公共成员可以在类的外部被访问。在前面的 Rectangle 类示例中,widthheightarea 函数都是公共成员,所以在类的外部可以通过对象来访问它们。
  2. private(私有的):私有成员只能在类的内部被访问,类外部的代码无法直接访问私有成员。这有助于数据的隐藏和保护。例如,修改 Rectangle 类如下:
class Rectangle {
private:
    int width;
    int height;
public:
    int area() {
        return width * height;
    }
};

在这个版本中,widthheight 变成了私有成员,外部代码不能直接访问它们。但是,area 函数仍然可以访问,因为它是类的内部成员。 3. protected(受保护的):受保护成员与私有成员类似,它们不能在类外部直接访问。但是,受保护成员可以被派生类(从该类继承而来的类)访问。这在继承的场景中非常有用,我们后续会详细介绍。

类的数据成员

数据成员是类中定义的变量,用于存储对象的状态信息。它们可以是基本数据类型(如 intfloatchar 等),也可以是用户自定义的数据类型(如其他类的对象)。

  1. 初始化数据成员
    • 可以在类定义时给数据成员赋初始值(C++11 及以后支持)。例如:
class Circle {
public:
    double radius = 1.0;
    double getArea() {
        return 3.14159 * radius * radius;
    }
};
- 也可以在构造函数中初始化数据成员。构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的数据成员。以下是通过构造函数初始化 `Circle` 类数据成员的示例:
class Circle {
public:
    double radius;
    Circle(double r) {
        radius = r;
    }
    double getArea() {
        return 3.14159 * radius * radius;
    }
};
  1. 静态数据成员: 静态数据成员是属于类的所有对象共享的数据成员,而不是每个对象都有自己的副本。它们通过在数据成员声明前加上 static 关键字来定义。例如:
class Student {
public:
    static int totalStudents;
    Student() {
        totalStudents++;
    }
};
int Student::totalStudents = 0;

在上述代码中,totalStudents 是一个静态数据成员,用于记录 Student 类创建的对象总数。在类外部需要对静态数据成员进行初始化。每个 Student 对象创建时,totalStudents 都会增加。

类的成员函数

成员函数是定义在类内部的函数,它们可以访问类的所有成员(包括私有成员)。成员函数可以在类定义内部定义,也可以在类定义外部定义。

  1. 在类定义内部定义成员函数:前面的 RectangleCircle 类示例中,areagetArea 函数都是在类定义内部定义的。这种方式定义的函数会被自动视为内联函数(除非函数体过大),编译器会尝试将函数调用替换为函数体的代码,以提高执行效率。

  2. 在类定义外部定义成员函数:如果成员函数的定义比较复杂,为了保持类定义的简洁性,可以在类定义外部定义成员函数。例如,对于 Rectangle 类的 area 函数,我们可以这样定义:

class Rectangle {
public:
    int width;
    int height;
    int area();
};

int Rectangle::area() {
    return width * height;
}

在类定义外部定义成员函数时,需要使用作用域解析运算符 :: 来指定函数所属的类。

  1. 成员函数的参数和返回值:成员函数可以接受参数并返回值,与普通函数类似。例如,我们可以给 Rectangle 类添加一个函数,用于判断矩形是否为正方形:
class Rectangle {
public:
    int width;
    int height;
    bool isSquare() {
        return width == height;
    }
    bool isBiggerThan(Rectangle other) {
        return area() > other.area();
    }
    int area() {
        return width * height;
    }
};

isBiggerThan 函数中,它接受另一个 Rectangle 对象作为参数,并比较两个矩形的面积。

  1. 静态成员函数:静态成员函数是属于类而不是属于某个对象的成员函数。它们只能访问静态数据成员和其他静态成员函数。静态成员函数通过在函数声明前加上 static 关键字来定义。例如:
class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

在上述代码中,add 是一个静态成员函数,可以直接通过类名来调用,而不需要创建 MathUtils 类的对象:int result = MathUtils::add(3, 5);

C++ 类的使用

创建对象

类定义完成后,就可以使用类来创建对象。对象是类的实例,每个对象都拥有类中定义的数据成员的副本。创建对象的语法与定义变量类似,只不过数据类型是类名。例如,对于前面定义的 Rectangle 类:

Rectangle rect1;
rect1.width = 5;
rect1.height = 3;
int area1 = rect1.area();

在上述代码中,Rectangle rect1; 创建了一个名为 rect1Rectangle 类对象。然后通过对象名 . 成员名的方式访问对象的公共数据成员 widthheight 并赋值,最后调用对象的成员函数 area 来计算矩形的面积。

如果类有构造函数,在创建对象时会自动调用构造函数进行初始化。例如,对于 Circle 类:

Circle circle1(2.0);
double area2 = circle1.getArea();

这里通过 Circle circle1(2.0); 创建了一个半径为 2.0Circle 对象 circle1,构造函数 Circle(double r) 被调用,将半径初始化为 2.0

对象数组

可以定义对象数组,即数组中的每个元素都是一个对象。例如,对于 Rectangle 类:

Rectangle rectArray[3];
rectArray[0].width = 1;
rectArray[0].height = 2;
rectArray[1].width = 3;
rectArray[1].height = 4;
rectArray[2].width = 5;
rectArray[2].height = 6;

for (int i = 0; i < 3; ++i) {
    int area = rectArray[i].area();
    std::cout << "Rectangle " << i + 1 << " area: " << area << std::endl;
}

在上述代码中,Rectangle rectArray[3]; 创建了一个包含 3 个 Rectangle 对象的数组。然后分别对数组中的每个对象的 widthheight 进行赋值,并调用 area 函数计算并输出每个矩形的面积。

指向对象的指针

可以使用指针来指向对象。通过指针访问对象的成员时,需要使用 -> 运算符。例如,对于 Circle 类:

Circle *circlePtr = new Circle(3.0);
double area3 = circlePtr->getArea();
delete circlePtr;

在上述代码中,Circle *circlePtr = new Circle(3.0); 使用 new 运算符在堆上创建了一个 Circle 对象,并将对象的地址赋值给指针 circlePtr。然后通过 circlePtr->getArea() 调用对象的成员函数 getArea 来计算面积。最后,使用 delete 运算符释放动态分配的内存,以避免内存泄漏。

类的嵌套

一个类可以在另一个类的内部定义,这种类被称为嵌套类。例如:

class Outer {
public:
    class Inner {
    public:
        int value;
        Inner(int v) : value(v) {}
    };
    Inner createInner(int v) {
        return Inner(v);
    }
};

在上述代码中,Inner 类是 Outer 类的嵌套类。Outer 类的成员函数 createInner 可以创建 Inner 类的对象。使用嵌套类时,可以通过外层类名来访问内层类。例如:

Outer outer;
Outer::Inner inner = outer.createInner(10);

友元函数和友元类

有时候,我们可能需要让类外部的函数或类访问类的私有成员。这时可以使用友元函数或友元类。

  1. 友元函数: 友元函数是在类定义中通过 friend 关键字声明的非成员函数,它可以访问类的私有和保护成员。例如:
class Box {
private:
    int length;
    int width;
    int height;
public:
    Box(int l, int w, int h) : length(l), width(w), height(h) {}
    friend int volume(Box box);
};

int volume(Box box) {
    return box.length * box.width * box.height;
}

在上述代码中,volume 函数是 Box 类的友元函数,它可以访问 Box 类的私有成员 lengthwidthheight 来计算盒子的体积。

  1. 友元类: 友元类的所有成员函数都是另一个类的友元函数,因此可以访问另一个类的私有和保护成员。例如:
class ClassA {
private:
    int data;
public:
    ClassA(int d) : data(d) {}
    friend class ClassB;
};

class ClassB {
public:
    void display(ClassA a) {
        std::cout << "Data in ClassA: " << a.data << std::endl;
    }
};

在上述代码中,ClassBClassA 的友元类,所以 ClassB 的成员函数 display 可以访问 ClassA 的私有成员 data

类的构造函数和析构函数

构造函数

构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象的数据成员。构造函数的名称与类名相同,并且没有返回类型(包括 void)。

  1. 默认构造函数: 如果类中没有显式定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数不接受参数,并且不会对数据成员进行显式初始化(基本数据类型成员的值是未定义的)。例如:
class Point {
public:
    int x;
    int y;
};

Point p1; // 使用默认构造函数创建对象
  1. 带参数的构造函数: 为了对数据成员进行有意义的初始化,可以定义带参数的构造函数。例如:
class Point {
public:
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
};

Point p2(10, 20); // 使用带参数的构造函数创建对象

在上述代码中,Point(int a, int b) : x(a), y(b) {} 是一个带参数的构造函数,它使用初始化列表 : x(a), y(b) 来初始化数据成员 xy

  1. 构造函数的重载: 一个类可以有多个构造函数,只要它们的参数列表不同,这被称为构造函数的重载。例如:
class Circle {
public:
    double radius;
    Circle() : radius(0.0) {}
    Circle(double r) : radius(r) {}
};

Circle c1; // 使用默认构造函数
Circle c2(5.0); // 使用带参数的构造函数

在上述代码中,Circle 类有两个构造函数,一个是默认构造函数,另一个是带参数的构造函数。

析构函数

析构函数也是一种特殊的成员函数,它在对象被销毁时自动调用,用于释放对象占用的资源(如动态分配的内存)。析构函数的名称是在类名前加上波浪号 ~,并且没有参数和返回类型。

  1. 默认析构函数: 如果类中没有显式定义析构函数,编译器会自动生成一个默认析构函数。默认析构函数通常用于释放对象的数据成员所占用的资源(如果数据成员是对象,会调用其析构函数)。例如:
class MyClass {
public:
    int value;
};

MyClass obj; // 创建对象
// 对象生命周期结束时,默认析构函数被调用(这里没有需要释放的资源)
  1. 自定义析构函数: 当类中有动态分配的资源时,需要自定义析构函数来释放这些资源,以避免内存泄漏。例如:
class String {
private:
    char *str;
public:
    String(const char *s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    ~String() {
        delete[] str;
    }
};

在上述代码中,String 类的构造函数动态分配内存来存储字符串,析构函数 ~String() 使用 delete[] 来释放动态分配的内存。

类的继承

继承的概念

继承是面向对象编程的重要特性之一,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承成员。派生类可以继承基类的所有成员(除了构造函数和析构函数),并且可以添加新的成员或重写基类的成员。继承的语法如下:

class BaseClass {
public:
    int baseData;
    void baseFunction() {
        std::cout << "Base function" << std::endl;
    }
};

class DerivedClass : public BaseClass {
public:
    int derivedData;
    void derivedFunction() {
        std::cout << "Derived function" << std::endl;
    }
};

在上述代码中,DerivedClass 是从 BaseClass 派生而来的。public 是继承方式,它指定了基类成员在派生类中的访问权限。这里 public 继承表示基类的 public 成员在派生类中仍然是 public 的,基类的 protected 成员在派生类中仍然是 protected 的,基类的 private 成员在派生类中是不可访问的。

继承方式

C++ 中有三种继承方式:publicprivateprotected

  1. public 继承: 如前面的示例,在 public 继承中,基类的 public 成员在派生类中是 public 的,protected 成员在派生类中是 protected 的,private 成员在派生类中不可访问。这是最常用的继承方式,它保持了基类成员的原有访问级别,使得派生类对象可以像基类对象一样使用基类的 public 成员。

  2. private 继承: 在 private 继承中,基类的 publicprotected 成员在派生类中都变成 private 成员,private 成员仍然不可访问。例如:

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : private Base {
public:
    void accessBaseMembers() {
        publicData = 10;
        protectedData = 20;
        // privateData = 30; // 错误,无法访问
    }
};

在上述代码中,Derived 类通过 private 继承 Base 类,Base 类的 publicDataprotectedDataDerived 类中变成了 private 成员,只能在 Derived 类内部访问。

  1. protected 继承: 在 protected 继承中,基类的 public 成员在派生类中变成 protected 成员,protected 成员仍然是 protected 成员,private 成员不可访问。例如:
class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : protected Base {
public:
    void accessBaseMembers() {
        publicData = 10;
        protectedData = 20;
        // privateData = 30; // 错误,无法访问
    }
};

class FurtherDerived : public Derived {
public:
    void accessBaseMembers() {
        publicData = 10; // 错误,publicData 现在是 protected
        protectedData = 20;
    }
};

在上述代码中,Derived 类通过 protected 继承 Base 类,Base 类的 publicDataDerived 类中变成了 protected 成员。FurtherDerived 类从 Derivedpublic 继承,由于 publicDataDerived 类中是 protected 的,所以在 FurtherDerived 类中不能直接访问 publicData

派生类的构造函数和析构函数

  1. 构造函数: 派生类的构造函数首先会调用基类的构造函数来初始化从基类继承的成员,然后再初始化派生类自己的数据成员。例如:
class Animal {
public:
    std::string name;
    Animal(const std::string &n) : name(n) {
        std::cout << "Animal constructor: " << name << std::endl;
    }
};

class Dog : public Animal {
public:
    int age;
    Dog(const std::string &n, int a) : Animal(n), age(a) {
        std::cout << "Dog constructor: " << name << ", age " << age << std::endl;
    }
};

在上述代码中,Dog 类的构造函数 Dog(const std::string &n, int a) : Animal(n), age(a) 首先调用 Animal 类的构造函数来初始化 name 成员,然后初始化自己的 age 成员。

  1. 析构函数: 派生类的析构函数会在对象销毁时自动调用,并且在派生类析构函数执行完毕后,会自动调用基类的析构函数。例如:
class Animal {
public:
    std::string name;
    Animal(const std::string &n) : name(n) {
        std::cout << "Animal constructor: " << name << std::endl;
    }
    ~Animal() {
        std::cout << "Animal destructor: " << name << std::endl;
    }
};

class Dog : public Animal {
public:
    int age;
    Dog(const std::string &n, int a) : Animal(n), age(a) {
        std::cout << "Dog constructor: " << name << ", age " << age << std::endl;
    }
    ~Dog() {
        std::cout << "Dog destructor: " << name << ", age " << age << std::endl;
    }
};

在上述代码中,当 Dog 对象被销毁时,首先执行 Dog 类的析构函数,然后执行 Animal 类的析构函数。

多态性

多态性是面向对象编程的另一个重要特性,它允许通过基类指针或引用调用派生类的函数。C++ 中实现多态性主要通过虚函数和函数重写。

  1. 虚函数: 在基类中声明为 virtual 的函数称为虚函数。虚函数在派生类中可以被重写(重新定义)。例如:
class Shape {
public:
    virtual double area() {
        return 0.0;
    }
};

class Circle : public Shape {
public:
    double radius;
    Circle(double r) : radius(r) {}
    double area() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
public:
    double width;
    double height;
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() override {
        return width * height;
    }
};

在上述代码中,Shape 类的 area 函数被声明为虚函数。CircleRectangle 类继承自 Shape 类,并重写了 area 函数。

  1. 函数重写: 派生类中重新定义基类的虚函数称为函数重写。在 C++11 及以后,可以使用 override 关键字来显式标记重写的函数,这有助于编译器检查重写是否正确。例如,在上面的代码中,CircleRectangle 类的 area 函数使用了 override 关键字。

  2. 动态绑定: 通过基类指针或引用调用虚函数时,会根据对象的实际类型(而不是指针或引用的类型)来决定调用哪个函数,这被称为动态绑定。例如:

Shape *shape1 = new Circle(5.0);
Shape *shape2 = new Rectangle(4.0, 3.0);

double area1 = shape1->area();
double area2 = shape2->area();

delete shape1;
delete shape2;

在上述代码中,shape1shape2Shape 类型的指针,但它们分别指向 CircleRectangle 对象。当调用 area 函数时,实际调用的是 CircleRectangle 类中重写的 area 函数,这就是动态绑定的效果。

通过以上对 C++ 类的定义与使用的详细介绍,希望能帮助读者深入理解 C++ 面向对象编程的核心概念,并能够熟练运用类来开发高效、可维护的程序。在实际编程中,合理运用类的各种特性,如封装、继承和多态,能够大大提高代码的复用性和可扩展性。