类的基本写法就一句: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 的关系了。加回去,就好。这一删一加,:: 的作用就再也忘不了。
六、常见误区
- "类定义末尾不用分号"——必须用。这是 C++ 里最经典的"漏一个字符、报一屏错"的坑,和
struct一模一样(8.1.1)。 - "类外实现时,参数和返回值可以不用写全"——要写全。类外写实现时,函数头和类里声明的必须完全一致(返回类型、参数类型、参数个数)。唯一允许省略的是默认参数值(6.2.3):默认值写在声明里,实现里就不能再写一遍(写两遍会报重定义)。
- "类里写实现和类外写实现,功能有差别"——功能没差别,但有性能与工程上的差别:类内定义的函数默认内联(10.4.2,可省函数调用开销,但会让编译变慢、代码变大);类外定义的不自动内联。工程上的差别更大:类外实现才能放进
.cpp文件,实现改了别人不用重新编译(10.1.3)。 - "
::和.可以随便用"——不能。::左边必须是"类名/命名空间名",.左边必须是"对象名"(指针则用->,7.6.1)。用错会得到语法错误。 - "前向声明等于把类定义好了"——不等于。它只告诉编译器"有这个名字",不给任何细节。所以只能定义指针/引用,不能定义对象、不能访问成员;真正要用到细节的地方,还得
#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`**。- ☐ 10.1.3 类的实现(下一节)