Overview

  1. 课程介绍与评分体系 (Course Overview & Grading)
  2. C++ 中的变量与基本类型 (Variables & Types)
  3. 封装 (Encapsulation) 与 类的定义 (Class Structure) —— 结合 sphere.h / sphere.cpp
  4. 命名空间 (Namespaces) —— 结合代码中的 cs225
  5. 构造函数 (Constructors) —— 默认构造函数 vs 自定义构造函数
  6. 指针入门 (Pointers Introduction)

C++ 中的变量与基本类型 (Variables & Types)

变量的四大属性

“Every variable is defined by four properties”

  1. Name (名称): 变量的标识符(例如 myFavoriteInt, s
  2. Type (类型): 它是什么样的数据(例如 int, Sphere),类型决定了它占用多少内存以及如何解释这块内存
  3. Value (值): 存储的具体内容(例如 42, 半径为 1.0
  4. Location (地址/内存位置): 它在计算机内存中的具体位置

变量的两大分类

C++ 是强类型 (Strongly Typed) 的语言,变量分为两类 :

  1. 基本类型 (Primitive/Fundamental Types):
    • 语言内置的,直接支持的类型
    • 例子:
      • int myFavoriteInt; (整数)
      • char grade = 'A'; (字符)
      • double gamma = 0.653; (双精度浮点数)
  2. 用户定义类型 (User-Defined Types / Class Types):
    • 创建新的结构来存储数据
    • 例子:
      • Sphere myFavoriteSphere; (球体对象)
      • Cube rubix; (立方体)
      • Grade courseGrade; (成绩对象)

封装 (Encapsulation) 与类的定义 (Class Structure)

封装

Encapsulation is a separation of interface from implementation.

封装带来的两大好处

  1. 隐藏复杂性 (Hides complexity):用户只需要知道它能做什么(接口),而不需要知道它是怎么做的(实现)
  2. 数据保护 (Allows private data to be private):防止外部代码随意篡改对象的内部状态

C++ 的实现方式:

C++ 在语言层面拥抱了封装,将接口 (Interface) 定义在 .h 文件中,将实现 (Implementation) 写在 .cpp 文件中

sphere.h (接口文件/头文件)

sphere.h 讲清楚用户定义的Sphere 类长什么样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef SPHERE_H   // 1. Header Guards (防止重复包含)
#define SPHERE_H // 文件名全大写 .转成_

namespace cs225 { // 2. Namespace

class Sphere { // 3. 类声明
public: // 4. Public (公共接口)
Sphere();
Sphere(double r);
double getRadius();
double getVolume();

private: // 5. Private (私有数据)
double r_;
};
}
#endif

解析:

  1. Header Guards (#ifndef, #define, #endif): 这是一个标准的 C++ 惯例,防止同一个头文件被多次 #include,避免编译错误
  2. class Sphere { ... };: 定义一个新的类型 Sphere,类定义的最后必须有一个分号 ;
  3. public: 列出的函数是公开的,任何人(包括 main.cpp)都可以调用这些函数,比如 s.getRadius() ,构成了接口
  4. private: 列出的变量是私有的只有 Sphere 类内部的函数可以访问,外部代码(如 main.cpp)如果试图直接访问 s.r_,编译器会报错,这实现了数据保护
    • 这里的成员变量名为 r_在 C++ 中,习惯给私有成员变量加下划线后缀(或前缀),以便与普通变量区分

sphere.cpp (实现文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "sphere.h" // 1. 包含对应的头文件

namespace cs225 {

// 2. 作用域解析运算符 (::)
Sphere::Sphere() {
r_ = 1.0;
}

// ... (构造函数稍后讲)

double Sphere::getRadius() {
return r_;
}

double Sphere::getVolume() {
return (4 r_ r_ r_ 3.14159265) / 3.0;
}
}

解析:

  1. #include "sphere.h": .cpp 文件必须知道它要实现的类长什么样,所以必须包含头文件
  2. Sphere:: (Scope Resolution Operator):
    • 它告诉编译器下面定义的这个 getRadius 函数,不是一个普通的全局函数,而是属于 Sphere 类的成员函数
    • 正因为有了 Sphere::,在这个函数内部,才能直接使用 r_ 这个私有变量

命名空间 (Namespaces)

为什么要用命名空间?

世界上可能有很多其他的 Sphere 类,如何区分它们

  • 命名空间就像是文件夹或者人的姓氏
  • CS225 课程里的 Sphere 是 cs225::Sphere(cs225 家族的 Sphere)
  • 标准库里的 vectorstd::vector(std 家族的 vector)

代码中的实现

sphere.h

1
2
3
4
5
namespace cs225 {  // 打开命名空间
class Sphere {
// ...
};
} // 关闭命名空间

这就好比把 Sphere 类定义在了一个叫 cs225 的包裹里

sphere.cpp 实现函数时,也必须包裹在同样的命名空间里,或者明确指出是谁:

1
2
3
4
namespace cs225 {
Sphere::Sphere() { ... }
// ...
}

如何使用?(main.cpp)

方式 1:全名调用 (Explicitly)

这是最严谨的写法

1
2
3
4
int main() {
cs225::Sphere s; // 要用 cs225 里的那个 Sphere
return 0;
}

方式 2:使用 using 指令 (Simplify the Syntax)

如果在 main.cpp 里一直写 cs225:: 会很麻烦,告诉编译器:如果在当前文件里找不到某个名字,就去 cs225 或者 std 命名空间里找找看

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include "sphere.h"

using namespace std; // 导入 std 命名空间 (让可以直接写 cout, endl)
using namespace cs225; // 导入 cs225 命名空间 (让可以直接写 Sphere)

int main() {
Sphere s; // 编译器自动识别为 cs225::Sphere
cout << "Radius: " << s.getRadius() << endl; // 编译器自动识别为 std::cout
return 0;
}

构造函数 (Constructors)

什么是构造函数

  • 构造函数是当创建一个类的新对象时,被调用的第一个函数,它的主要任务是:初始化对象的状态(给变量赋初值)
  • 它的名字和类名完全相同(例如 Sphere
  • 它没有返回值类型(连 void 都不写)

三种构造函数的情况

场景 1:自动默认构造函数 (Automatic Default Constructor)

如果 sphere.h 里完全没有写任何构造函数,C++ 编译器会有一个自动默认构造函数

  • 它不需要参数
  • 它不做任何实质性的初始化工作(对于基本类型如 double,它的值可能是内存里的垃圾值,这很危险)
  • 只有当没定义任何构造函数时,C++ 才会有这个 constructor

场景 2:自定义默认构造函数 (Custom Default Constructor)

sphere.cpp 中明确定义了一个不带参数的构造函数:

1
2
3
4
5
6
7
// sphere.h
Sphere();

// sphere.cpp
Sphere::Sphere() {
r_ = 1.0; // 手动把半径设为 1.0
}

当写 Sphere s; 时,这个函数会被调用此时 s 的半径就是 1.0,不再是随机垃圾值

场景 3:带参数的构造函数 (Custom Parameterized Constructor)

定义了允许用户指定半径的构造函数 :

1
2
3
4
5
6
7
// sphere.h
Sphere(double r);

// sphere.cpp
Sphere::Sphere(double r) {
r_ = r; // 使用用户提供的 r 来初始化成员变量 r_
}

当写 Sphere s(5.0); 时,这个函数被调用,s 的半径就是 5.0

注意

假设只定义了带参数的构造函数 Sphere(double r),而删掉了不带参数的 Sphere()

main.cpp

1
2
3
4
int main() {
Sphere s; // 报错 编译失败
// ...
}

为什么会报错?

  1. 写了 Sphere s;,这意味着想调用不带参数的构造函数
  2. 但是在代码里删掉了 Sphere()
  3. 编译器以为用户想要用默认构造函数,但是自己定义了一个 Sphere(double r) 但是由于有定义的带参数的构造函数,就不存在自动默认构造函数了

结论:如果定义了带参数的构造函数,并且还希望用户能用 Sphere s; 这种方式创建对象,就必须显式地把 Sphere() 写出来

指针入门 (Pointers Introduction)

什么是指针

普通变量存储的是数据本身,而指针存储的是数据的内存地址 (Memory Address) ,可以想象成一个路标或门牌号,它指向真正存放数据的地方

语法符号

  1. * (声明指针): 放在类型后面,表示这是一个指针变量
    • Sphere s1; 说明 s1 是一个球体对象
    • Sphere *s2; 说明 s2 是一个指向球体对象的指针
  2. & (取地址符): 获取某个变量在内存中的地址
    • s2 = &s1; -> 把 s1 的地址赋值给 s2 ,现在 s2 指向了 s1
  3. -> (箭头运算符): 指针的成员访问符

如何访问成员?(. Vs ->)

  • 对象 (Object) -> 使用 点号 (.)

    1
    2
    Sphere s1;
    s1.getRadius(); // 正确
  • 指针 (Pointer) -> 使用 箭头 (->)

    1
    2
    3
    Sphere s2 = &s1;
    s2->getRadius(); // 正确
    // s2.getRadius(); // 错误!s2 只是个地址,本身没有 radius

s2->getRadius() 的意思是:顺着 s2 指向的地址找到那个对象,然后调用它的 getRadius

不使用箭头 ->,用指针访问成员

先解引用 (Dereference): (*s2).getRadius()

  • *s2 表示取出 s2 指向的那个对象(也就是 s1
  • 既然取出了对象,就可以用点号 .
  • 注意:括号 () 不能省,因为 . 的优先级比 *

总结:s2->getRadius() 等价于 (s2).getRadius(),但前者更简洁,是 C++ 通用写法

Q&A

宏 (Macro) 与 头文件保护 (Header Guard)

  1. 什么叫 Macro (宏)
    1. 在 C++ 中,以 # 开头的指令(如 #include , #define )都不是给编译器看的,而是给预处理器 (Preprocessor) 看的,预处理器就像一个文本替换工具(类似 Word 里的查找替换),它在真正编译代码之前工作
    2. #define SPHEREH 的意思就是:在预处理器的记事本上,记下一个叫 SPHEREH 的标记(Flag)它不代表具体的值,它只是存在而已
  2. 每一个头文件都需要 #define
    1. 几乎每个头文件都需要,是为了防止代码被重复粘贴导致的冲突
    2. 当写 #include "sphere.h" 时,预处理器把 sphere.h 里的代码原封不动地复制粘贴到 main.cpp 里,它不管文件名叫什么
    3. 为什么要防重复: 假设有一个 math.h 引用了 sphere.h, main.cpp 同时也引用了 sphere.h ,如果不加保护,sphere.h 的代码会被复制粘贴两次,进入 main.cpp,编译器看到两个 class Sphere { … }; 定义时,就会报错:Sphere 类重定义!(Class redefinition error)
  3. Header Guard 的工作流程: 这三行代码是一个整体,缺一不可 :
1
2
3
4
5
6
7
#ifndef SPHEREH // 1. 检查:如果没有定义过 SPHEREH 这个标记...
#define SPHEREH // 2. 标记:那现在就定义 SPHEREH (像在手背盖个章)

class Sphere { ... }; // 代码内容

#endif // 3. 结束:if 结束

  1. 作用域解析运算符 Sphere::Sphere()
    1. :: (Scope Resolution Operator)
    2. sphere.h 是声明 (Declaration):像是菜单,告诉大家有这道菜
    3. sphere.cpp 是定义 (Definition):像是厨房,具体做这道菜
    4. 在 sphere.cpp 文件里,在写代码时,默认是在全局环境下的
1
2
3
4
// 错误的写法
double getRadius() {
return r; // 报错!谁是 r?
}

编译器会认为在写一个普通的全局函数 getRadius,这个函数不属于任何类,也就找不到 Sphere 类里的私有变量 r

所以,需要用 :: 来找到函数的来源:

`

1
2
3
4
5
// 正确的写法
// ↓ 属于哪个类 ↓ 函数名叫什么
double Sphere :: getRadius() {
return r; // 现在编译器知道在 Sphere 类里面,自然能找到 r
}

对于构造函数 Sphere::Sphere() 也是同理:

  • 第一个 Sphere:类名(是哪个家族的)
  • ::属于
  • 第二个 Sphere:函数名(构造函数的名字和类名一样)
  • Sphere::Sphere() ` 的意思是 —— 现在要定义属于 Sphere 类的那个名为 Sphere 的构造函数