Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

类与对象

C++ 是一种支持面向对象编程(Object-Oriented Programming, OOP)的语言。 在面向对象的设计思想中,类(Class)是 C++ 的核心特性之一,常被称为用户自定义的数据类型(user-defined type)。

类用于定义对象的结构与行为,是一种将数据与操作这些数据的函数封装在一起的抽象描述。 在类中,用于存储数据的部分称为成员变量(Member Variables),而用于操作这些数据的函数称为成员函数(Member Functions)。

类本质上是一种模板(Template)或蓝图(Blueprint),通过它可以创建出多个具有相同属性和行为的具体实例,这些实例被称为对象(Objects)。每个对象都拥有属于自己的成员变量副本,并可以通过成员函数来执行特定的操作。

一、类的结构

在 C++ 中,类(Class)是一种由用户定义的数据类型(User-defined Data Type), 它将数据(成员变量)与操作数据的函数(成员函数)有机地结合在一起, 从而实现封装(Encapsulation)与抽象(Abstraction)。

一个类的基本结构如下:

class ClassName {
private:
    // 私有成员(数据与函数)
protected:
    // 受保护成员
public:
    // 公有成员
};

1. 类的基本组成

类由以下几个主要部分构成:

组成部分说明
类名(Class Name)类的标识符,用于定义和引用该类。
成员变量(Member Variables)用于存储对象状态的数据。
成员函数(Member Functions)用于操作数据或定义对象行为的函数。
访问控制符(Access Specifiers)控制外部对类成员的访问权限:private、protected、public。
构造函数与析构函数(Constructor & Destructor)对象创建与销毁时自动调用的特殊成员函数。
静态成员(Static Members)属于类本身而非某个对象的成员。
友元(Friend)特殊访问权限,允许外部函数或类访问私有成员。

2. 访问控制符(Access Specifiers)

访问控制符用于限定类成员的可见性和访问范围:

控制符访问范围典型用途
private仅类内可访问封装内部实现细节
protected类内和子类可访问继承时保留部分访问权限
public任何地方都可访问提供对外接口

示例:

class Example {
private:
    int secret;          // 私有成员
protected:
    int semi_secret;     // 受保护成员
public:
    int visible;         // 公有成员
};

默认情况下,class 的成员默认是 private, 而 struct 的成员默认是 public。

3. 成员变量(Member Variables)

成员变量用于存储对象的状态。 每个对象都会拥有独立的一份成员变量副本。

class Student {
private:
    std::string name;
    int age;
};

成员变量可以是基本类型、对象、指针、引用、数组、容器等。

也可以为成员变量提供默认初始值(C++11 起支持):

class Point {
    int x = 0;
    int y = 0;
};

4. 成员函数(Member Functions)

成员函数用于定义对象的行为,通常用于访问和修改成员变量。

class Student {
private:
    std::string name;
    int age;

public:
    void setInfo(const std::string& n, int a) {
        name = n;
        age = a;
    }

    void display() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

也可以在类外定义成员函数:

void Student::display() {
    std::cout << "Name: " << name << ", Age: " << age << std::endl;
}

5. 构造函数(Constructor)

构造函数在对象创建时自动执行,用于初始化成员变量。 它与类同名,无返回值。

class Student {
private:
    std::string name;
    int age;

public:
    // 构造函数
    Student(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Constructor called." << std::endl;
    }
};

构造函数的种类包括:

类型说明示例
默认构造函数无参版本Student()
有参构造函数初始化时传参Student("张三", 20)
拷贝构造函数用已有对象创建新对象Student(const Student& s)

构造函数支持多态,可以使用多个参数不同的构造函数,在使用时会自动匹配。

6. 析构函数(Destructor)

析构函数在对象销毁时自动调用,用于资源释放或清理。

class Student {
public:
    ~Student() {
        std::cout << "Destructor called." << std::endl;
    }
};

析构函数名以 ~ 开头,无参、无返回值,每个类最多有一个析构函数。

7. this 指针

this 是一个隐含指针,指向调用成员函数的当前对象。 它可用于区分成员变量与同名参数:

class Student {
private:
    std::string name;

public:
    Student(const std::string& name) {
        this->name = name; // 区分成员变量与参数
    }
};

8. 静态成员(Static Members)

静态成员属于类本身,而非具体对象。 所有对象共享同一份静态数据。

静态成员分为两类:

  1. 静态成员变量(Static Member Variables)
  2. 静态成员函数(Static Member Functions)

(1)静态成员变量

静态成员变量在所有对象之间共享同一份存储空间。 它不依赖于任何对象存在,无论创建多少个对象,这个变量都只有一份。

因此,静态成员变量常用于表示类级别的公共信息,例如计数器、配置、全局状态等。

class Counter {
public:
    static int count;
    Counter() { count++; }
};

int Counter::count = 0;

(2)静态函数变量

静态成员函数同样属于类本身,而不是对象。 它的主要特征是:

  • 不依赖任何对象实例
  • 无法访问非静态成员变量或函数(因为没有具体对象可供操作)
  • 通常用于类级别的操作,如访问静态数据或执行与对象无关的逻辑。
  • 没有 this 指针,因为它不属于任何对象
class Counter {
private:
    static int count;

public:
    Counter() { count++; }

    // 静态成员函数
    static void showCount() {
        std::cout << "Current count: " << count << std::endl;
    }
};

// 类外定义静态变量
int Counter::count = 0;

int main() {
    Counter a, b;
    Counter::showCount();  // 通过类名访问
    a.showCount();         // 也可通过对象访问
}

9. 常成员(const Members)

  • 常成员函数:在函数后加 const,表示不修改成员变量。
  • 常对象:对象一旦定义,其状态不可更改。
class Point {
private:
    int x, y;

public:
    void display() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

10. 友元(Friend)

友元函数或友元类可以访问类的私有成员。 这在操作符重载、类间紧密协作时非常有用。

#include <iostream>

class Box {
private:
    int width;
public:
    void set_width(int val){
        width = val;
    }
    friend void printWidth(Box b);
};

void printWidth(Box b) {
    std::cout << "Width: " << b.width << std::endl;
}

int main(){
    Box a;
    a.set_width(20);
    printWidth(a);
}

友元会破坏封装性,应谨慎使用。

友元函数不是成员函数,不能用 对象.函数() 方式调用。它只是可以访问类私有成员的普通函数,调用方式与普通函数相同。

11. 类的组合与嵌套

一个类可以将另一个类作为成员,这种关系称为组合(Composition)。

class Address {
public:
    std::string city;
};

class Person {
public:
    std::string name;
    Address addr;  // 组合
};

当对象销毁时,其组合成员也会自动销毁。

二、对象

在 C++ 中,对象(Object)是类的实例(Instance)。 当我们定义了一个类后,这个类本身只是一个抽象的模板或蓝图, 而对象才是真正占用内存、可操作的具体实体。

类定义了“事物的共性”,对象体现了“事物的个性”。

1. 对象的创建与定义

定义一个类对象与定义普通变量非常相似:

class Student {
public:
    std::string name;
    int age;
};

int main() {
    Student s1;  // 创建对象 s1
    s1.name = "张三";
    s1.age = 20;

    Student s2;  // 再创建一个对象 s2
    s2.name = "李四";
    s2.age = 21;
}

每个对象都有独立的成员变量副本,互不影响。

例如:

s1.age = 20;
s2.age = 21;
// 修改 s1 的 age 不会影响 s2

2. 对象的初始化

当类中定义了构造函数时,对象可以在定义时直接初始化:

class Point {
private:
    int x, y;

public:
    Point(int a, int b) {  // 构造函数
        x = a;
        y = b;
    }

    void show() {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Point p1(1, 2);  // 调用构造函数
    Point p2 = Point(3, 4); // 另一种写法
    p1.show();
    p2.show();
}

注意:对象创建时,构造函数会被自动调用;对象销毁时,析构函数会被自动调用。

3. 对象的作用域与生命周期

对象的生命周期与其定义的位置有关。

定义位置生命周期说明
局部对象(栈上)作用域结束时自动销毁
全局对象程序结束时销毁
静态对象程序结束时销毁
动态对象(堆上)需要手动释放(使用 new / delete)

示例:

class Example {
public:
    Example() { std::cout << "Constructed\n"; }
    ~Example() { std::cout << "Destructed\n"; }
};

int main() {
    Example local;            // 局部对象
    static Example global;    // 静态对象
    Example* ptr = new Example();  // 动态对象

    delete ptr; // 手动释放
}

输出顺序体现了不同对象的生命周期管理。

4. 对象数组

可以定义一个包含多个对象的数组:

class Point {
public:
    int x, y;
    Point(int a = 0, int b = 0) : x(a), y(b) {}
};

int main() {
    Point arr[3] = { {1,2}, {3,4}, {5,6} };
    for (auto& p : arr)
        std::cout << "(" << p.x << ", " << p.y << ")\n";
}

若类中没有默认构造函数,则必须在定义对象数组时显式提供初始化参数。

5.对象的指针与引用

(1)对象指针

和基本类型类似,可以使用指针指向对象。

Student s1;
Student* ptr = &s1;
ptr->name = "王五";
ptr->age = 22;

使用 -> 运算符访问对象的成员。

也可以使用 new 创建动态对象:

Student* stu = new Student;
stu->name = "赵六";
stu->age = 18;
delete stu; // 释放内存

(2)对象引用

引用可以直接操作已有对象:

Student s1;
Student& ref = s1;
ref.name = "张三";

引用不会创建新对象,只是为已有对象取别名。

6. 对象的拷贝与赋值

当我们用一个对象初始化另一个对象时,会自动调用拷贝构造函数。

class Box {
public:
    int width;
    Box(int w) : width(w) {}
    Box(const Box& b) { // 拷贝构造函数
        width = b.width;
        std::cout << "Copy constructor called\n";
    }
};

int main() {
    Box b1(10);
    Box b2 = b1; // 调用拷贝构造函数
}

赋值操作调用的是赋值运算符(operator=),而不是拷贝构造。

7. const 对象

可以将对象声明为常量,使其内容不可被修改:

class Point {
public:
    int x, y;
    void show() const {
        std::cout << x << ", " << y << std::endl;
    }
};

int main() {
    const Point p = {1, 2};
    p.show();
    // p.x = 5; // 错误:常对象不可修改成员
}

常对象只能调用常成员函数(即声明为 void func() const 的函数)。

8. 对象之间的比较与赋值

对象可以相互赋值:

Student s1, s2;
s1.name = "A";
s2 = s1; // 成员变量逐个拷贝

但若希望对象间的比较(==、< 等)有意义,需要重载运算符(进阶内容,后续讲述)。

9. 对象与内存模型

每个对象在内存中都有独立的成员变量副本:

Student s1 ──► name="张三", age=20
Student s2 ──► name="李四", age=21

而静态成员在所有对象间共享。

三、完整示例

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int age;

    Student(string n = "未知", int a = 0) : name(n), age(a) {
        cout << "Constructed: " << name << endl;
    }

    ~Student() {
        cout << "Destructed: " << name << endl;
    }

    void show() const {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Student s1("张三", 20);
    Student s2("李四", 21);

    s1.show();
    s2.show();

    Student* p = new Student("王五", 22);
    p->show();
    delete p;  // 手动释放动态对象
}

输出:

Constructed: 张三
Constructed: 李四
Constructed: 王五
Name: 张三, Age: 20
Name: 李四, Age: 21
Name: 王五, Age: 22
Destructed: 王五
Destructed: 李四
Destructed: 张三

四、面向对象的三大特征

面向对象编程(OOP)的核心思想可以概括为三大特征:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。这三大特征是 C++ 类与对象设计的基础,也是理解和应用面向对象程序设计的关键。

继承与多态的内容属于C++面向对象进阶内容。

1. 封装(Encapsulation)

封装是面向对象最基本的特征之一,也是类和对象概念的核心。

封装的核心思想是将对象的状态(数据)和行为(函数)组合到一个整体——类中,并通过访问控制机制对外部可见性进行限制和保护,对受保护的私有数据仅可以使用成员函数进行操作。

作用:

  1. 对内部数据进行保护,只允许可信的方法或对象访问;
  2. 隐藏实现细节,使类的使用者无需了解内部工作原理;
  3. 提供统一接口,提高模块化和可维护性。

示例:

class Student {
private:
    std::string name; // 私有成员,外部无法直接访问
    int age;

public:
    void setInfo(const std::string& n, int a) { // 提供接口修改数据
        name = n;
        age = a;
    }

    void display() const { // 提供接口访问数据
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

在上例中,name 和 age 仅能通过 setInfo 和 display 访问和修改,这就是封装的典型应用。

封装就是把客观事物封装成抽象类,并控制外部访问权限,保护数据安全并提高代码可维护性。

2. 继承(Inheritance)

继承允许新建的类复用已有类的属性和行为,并在此基础上扩展。被继承的类称为基类(Base Class)或父类,从基类继承的类称为派生类(Derived Class)或子类。

按继承方式不同,基类成员在派生类中的访问级别会发生变化:

继承方式基类 public 成员基类 protected 成员基类 private 成员
public 继承publicprotected不可访问
protected 继承protectedprotected不可访问
private 继承privateprivate不可访问
class Person {
protected:
    std::string name;
public:
    void setName(const std::string& n) { name = n; }
};

class Student : public Person {   // Student 继承 Person
private:
    int grade;
public:
    void setGrade(int g) { grade = g; }
    void showInfo() const {
        std::cout << "Name: " << name << ", Grade: " << grade << std::endl;
    }
};

Student 复用了 Person 的 name,同时扩展了 grade。继承的主要作用是代码复用和构建类层次结构,它也是实现运行时多态的前提。

继承的完整规则——构造与析构顺序、多重继承、菱形继承与虚继承、using 声明改变访问级别等——见 继承。

需要说明的是,继承并非复用的唯一手段。当类之间是「has-a」(拥有)关系而非「is-a」(是)关系时,应当使用组合:在类中包含另一个类的对象作为成员。设计原则是优先使用组合,而非继承。

3. 多态(Polymorphism)

多态指同一个操作作用于不同对象时表现出不同的行为。C++ 中的多态分为两类:

类型实现方式决定时机
编译时多态(静态多态)函数重载、运算符重载、模板编译期
运行时多态(动态多态)虚函数运行期

运行时多态通过基类指针或引用调用虚函数实现:

class Animal {
public:
    virtual void speak() const { std::cout << "Animal sound" << std::endl; }
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Woof!" << std::endl; }
};

int main() {
    Animal* a = new Dog();
    a->speak();   // 输出 "Woof!",根据实际对象类型调用
    delete a;
}

关键在于 speak 被声明为 virtual:通过基类指针调用时,实际执行的是派生类的版本。多态使统一接口处理不同对象成为可能,提高了程序的灵活性和可扩展性。

虚函数的工作机制(虚函数表、动态绑定)、虚析构函数的必要性、纯虚函数与抽象类、override 与 final 等,见 虚函数与多态。

五、小结

封装、继承、多态构成了面向对象设计的基础:

  • 封装把数据和操作数据的方法组织在一起,并通过访问控制保护内部状态,本章已完整介绍;
  • 继承实现代码复用和类层次结构,深入内容见 继承;
  • 多态让统一接口能够处理不同对象,深入内容见 虚函数与多态。

关于运算符重载、友元、拷贝控制、模板高级应用等面向对象的进阶主题,见 面向对象进阶。