🔷 第十章 类和对象
📄 一句话概括

类的基本写法就一句:class 类名 { 成员 };——末尾这个分号千万别漏(和 struct 一样的经典坑,8.1.1 踩过)。里面的成员函数有两种落法:写在类里(隐式内联,10.4.2 细讲),或者类里只留声明、实现写到类外——后者必须用 类名::函数名 来标明"这个函数属于谁"。

📝 本节和前面几节的关系

10.1.1 讲了类里面有什么(成员变量/成员函数),本节把语法外壳补全:类怎么开头、怎么结尾、方法放哪儿写。:: 这个符号第一次正式登场——它叫作用域解析运算符,2.1.3 里写的 std::cout 用的就是它("cout 属于 std")。另外,本节会用到 6.1.1 的知识:函数有"声明"和"定义"两件事,这条规矩在类里同样成立。

一、最基本的写法

class 类名 {
private:                        // 访问修饰符
    类型 成员变量1;              // 成员变量
    类型 成员变量2;

public:
    返回类型 成员函数1(参数) {    // 成员函数(就地写完)
        // 实现
    }
    返回类型 成员函数2(参数);     // 成员函数(只声明,实现写在类外)
};                              // ← 这个分号,是新手最容易漏的

三条语法要点:

要点 说明
末尾分号 }; 不能省。漏了会报莫名其妙的错(编译器以为你还没写完,后面几十行的报错都成了"连带的")
大括号位置 类名后面直接跟 {,没有参数表、没有小括号
访问修饰符带冒号 private: public: protected:——冒号不能省,省了就成了"声明一个叫 private 的成员"

漏分号的典型现场(把 8.1.1 那个坑再踩一次,加深印象):

class Point {
public:
    int x, y;
}                    // ← 忘了分号

int main() {         // ← 编译器会在这里报一堆错
    ...
}

报错往往长这样:expected ';' before 'int'——报在 main 那一行。记住这条规律:漏了分号,报错通常出现在下一行或更远处(1.3.2 的铁律)。

二、成员函数:两种写法

写法一:就地写完(类内定义)

class Point {
private:
    int x, y;
public:
    void set(int a, int b) { x = a; y = b; }       // 直接写实现
    int  getX()            { return x; }
    int  getY()            { return y; }
};

特点:紧凑、看得清。这种函数会被编译器自动当作"内联函数"(10.4.2 专门讲这意味着什么)。

写法二:类里声明、类外实现

class Point {
private:
    int x, y;
public:
    void set(int a, int b);        // 只留"声明"(6.1.2 的规矩)
    int  getX();                   // 声明
    int  getY();                   // 声明
};

// ↓ 类外写实现:**必须用 类名:: 说明归属**
void Point::set(int a, int b) {
    x = a;                         // 注意:这里依然可以直接用 x、y
    y = b;
}

int Point::getX() { return x; }
int Point::getY() { return y; }

Point::set 读作"Point 里的 set"。

为什么类外还能直接用 x、y?因为 Point::set 被标明是 Point 的成员了——它依然在"家里",只是住在外面。(10.4.4 会揭晓:编译器偷偷给它传了一个"当前对象"的地址,就是靠这个。)

如果忘了写 Point:::

void set(int a, int b) { ... }      // 忘了 Point::

编译器会认为这是一个和 Point 毫无关系的普通全局函数——于是它里面的 x = a; 就找不到 x 了,报错 'x' was not declared in this scope(6.1.2 见过的脸)。看到这个错,先检查 类名:: 写了没有。

三、:: 是什么:作用域解析运算符

它回答的问题是:"这个名字,属于谁?"

std::cout                  // cout 属于 std 命名空间(2.1.3)
Point::getX                // getX 属于 Point 类
Point::count               // 如果 Point 有个静态成员 count(10.4.3)
::main                     // 全局的 main(前面什么都不写=全局作用域)
写法 含义
A::f() A 里面的 f
::f() 全局作用域的 f(明确"不要找类里的,要外面那个")
p.getX() 某个对象 p 的 getX(注意是点,不是双冒号)

区分清楚两个东西::: 是"类别归属"(哪个类/哪个命名空间),. 是"具体哪个对象"。Point::getX 是"Point 这个类里的 getX 这个函数",p.getX() 是"取 p 这个对象的 getX 结果"。一个指类,一个指对象。

四、类的"声明"和"定义"也分两件事

和函数一样(6.1.2),类也有"只声明"的写法——叫前向声明:

class Student;           // 前向声明:告诉编译器"有这么个类,细节待会儿再说"

class Course {
private:
    Student *p;          // 可以:用指针指向它(指针大小固定,不需要知道细节)
    // Student s;        // ❌ 不可以:编译器不知道 Student 多大,没法分配内存
};
能做什么 不能做什么
定义指向它的指针或引用 定义它的对象(要知道多大)
把它作为函数参数/返回值的类型 访问它的成员(要知道有哪些)

用途:解决两个类互相引用的死循环(A 里有 B 的指针、B 里有 A 的指针),或者减少头文件互相包含(10.1.3 会细讲)。

五、动手 Demo:一份类,两种写法

#include <iostream>
using namespace std;

// ───────── 类里只留声明 ─────────
class Rectangle {
private:
    double w, h;

public:
    void   set(double width, double height);     // 声明
    double area();                               // 声明
    double perimeter();                          // 声明
    void   show();                               // 声明
};                                               // ← 分号别漏

// ───────── 实现写在类外,每个都要带 Rectangle:: ─────────
void Rectangle::set(double width, double height) {
    w = (width  > 0) ? width  : 1;               // 3.1.8 的三目运算符
    h = (height > 0) ? height : 1;
}

double Rectangle::area()      { return w * h; }
double Rectangle::perimeter() { return 2 * (w + h); }

void Rectangle::show() {
    cout << "宽 " << w << " 高 " << h
         << " → 面积 " << area()
         << ",周长 " << perimeter() << endl;
}

int main() {
    Rectangle r;
    r.set(4, 3);
    r.show();            // 宽 4 高 3 → 面积 12,周长 14

    r.set(-5, 2);        // 非法的宽,set 里兜成 1
    r.show();            // 宽 1 高 2 → 面积 2,周长 6
    return 0;
}

看到什么算成功:

宽 4 高 3 → 面积 12,周长 14
宽 1 高 2 → 面积 2,周长 6

读结果:第二种写法(类外实现)跑起来和第一种完全一样。区别只是代码放在哪儿——而"放在哪儿"在多文件工程里是件大事(下一节 10.1.3 就开始分文件)。

30 秒实验:把第一个 void Rectangle::set(...) 里的 Rectangle:: 删掉(变成 void set(...)),重新编译。你会看到 'w' was not declared in this scope——因为编译器已经不认识它和 Rectangle 的关系了。加回去,就好。这一删一加,:: 的作用就再也忘不了。

六、常见误区

  1. "类定义末尾不用分号"——必须用。这是 C++ 里最经典的"漏一个字符、报一屏错"的坑,和 struct 一模一样(8.1.1)。
  2. "类外实现时,参数和返回值可以不用写全"——要写全。类外写实现时,函数头和类里声明的必须完全一致(返回类型、参数类型、参数个数)。唯一允许省略的是默认参数值(6.2.3):默认值写在声明里,实现里就不能再写一遍(写两遍会报重定义)。
  3. "类里写实现和类外写实现,功能有差别"——功能没差别,但有性能与工程上的差别:类内定义的函数默认内联(10.4.2,可省函数调用开销,但会让编译变慢、代码变大);类外定义的不自动内联。工程上的差别更大:类外实现才能放进 .cpp 文件,实现改了别人不用重新编译(10.1.3)。
  4. ":: 和 . 可以随便用"——不能。:: 左边必须是"类名/命名空间名",. 左边必须是"对象名"(指针则用 ->,7.6.1)。用错会得到语法错误。
  5. "前向声明等于把类定义好了"——不等于。它只告诉编译器"有这个名字",不给任何细节。所以只能定义指针/引用,不能定义对象、不能访问成员;真正要用到细节的地方,还得 #include 真头文件。
自测三问(点开对答案) 1. 类定义末尾为什么必须写分号?——因为 `class X { ... };` **整体是一条"声明语句"**,C++ 里语句要用分号收尾。漏了会让编译器认为定义没结束,**报错跑到后面几十行去**。 2. `Point::getX` 和 `p.getX()` 有什么区别?——`Point::getX` 用 **`::`(作用域解析符)**,表示"**Point 这个类**里的 getX"(说的是归属);`p.getX()` 用 **`.`(成员访问符)**,表示"**p 这个对象**的 getX"(说的是取谁的值)。 3. 类里只声明、类外写实现时,最容易漏什么?漏了会报什么错?——最容易漏 **`类名::`**。漏了以后里面的成员变量会找不到,报 **`'xxx' was not declared in this scope`**。

相关笔记