#面向对象程序设计01
编译环境和参考资料
本课程使用 C++23 标准。推荐编译器版本如下:
- GCC 14 或更高版本
- Clang 18 或更高版本
- MSVC 19.37,也就是 Visual Studio 2022 17.7 或更高版本
编译时需要使用:
1 | g++ -std=c++23 main.cpp |
也可以用 Compiler Explorer 在线编译和查看生成的汇编代码
C++ 是一门强调性能和底层控制的语言,很多时候只看源代码不够,还要知道它最终大概会生成什么样的机器级行为
第一个 C++23 程序
1 |
|
这段程序很短,但已经包含了几个重要概念。
#include <print> 告诉编译器需要使用 C++23 的格式化输出工具,#include <string> 引入标准库字符串类型
int main() 是程序入口,C++ 程序从 main 函数开始执行
int 表示这个函数返回一个整数,通常返回 0 表示程序正常结束;如果 main 结尾没有显式写 return 0;,C++ 会把它视为正常返回
std::string name = "Alice"; 创建了一个名叫 name 的变量,类型是 std::string,初始值是 `“Alice”``
`std::println("name : {}", name); 会把 {} 替换成后面的变量值,并自动换行
输入和输出
C++23 新增了 std::print 和 std::println:
1 | std::println("Hello, {}!", name); // 格式化输出并自动换行 |
它们比传统的 std::cout << ... 更接近现代语言里的格式化输出,也更容易读
输入仍然常用 std::cin 和 std::getline:
1 |
|
std::cin >> age 适合读取一个值,例如整数、浮点数或一个不含空格的单词std::getline(std::cin, name) 读取一整行,因此可以包含空格
这里要注意两类输入方式混用时,换行符残留可能导致 getline 读到空行
变量
1 | int age = 20; |
程序会做如下三件事:
- 在内存中分配一块空间,
int通常占 4 字节 - 给这块空间起名为
age - 把整数值
20存入这块空间
内存视图可以粗略写成:
1 | 地址 内容 名字 |
变量名是给程序员看的标签。程序编译之后,机器并不会按变量名查找数据,而是通过地址和偏移访问内存
不要使用未初始化变量
C++ 中读取未初始化的局部变量通常是未定义行为,结果可能看起来“偶尔能跑”,但这不是正确和好的
定义变量时尽量同时初始化:
1
2
3 int count{}; // 初始化为 0
double score{}; // 初始化为 0.0
std::string name; // 默认构造为空字符串
类型
类型规定了:
- 这块内存中的比特应该怎样被解释
- 这个值支持哪些操作
同样的二进制内容,如果按 int 解释、按 float 解释、按字符数组解释,得到的含义完全不同
常见类型如下:
| 类型 | 含义 | 典型大小 | 示例 |
|---|---|---|---|
int | 整数 | 4 字节 | int x = 42; |
double | 浮点数 | 8 字节 | double pi = 3.14; |
char | 单个字符 | 1 字节 | char c = 'A'; |
bool | 布尔值 | 1 字节 | bool ok = true; |
std::string | 文本 | 对象本身固定大小,内容可变长 | std::string s = "hello"; |
std::string 不是 C 语言里的 char*
一个 std::string 对象本身有固定大小,常见实现大约 24 到 32 字节;短字符串可能直接存放在对象内部,长字符串通常会在堆上另外申请空间,再由对象内部的指针指过去。我们使用它时不需要手动 malloc 和 free,这是标准库封装资源管理的价值
类型决定操作
类型决定了表达式能不能写,也决定了表达式是什么意思:
1 | int a = 10; |
a / b 是整数除法,小数部分会被丢弃
c / d 是浮点除法,会保留小数。它们看起来都是 /,但由于操作数类型不同,语义不同
auto
C++ 允许用 auto 让编译器根据初始值推断类型:
1 | auto x = 42; // int |
auto 不是动态类型,也不是“没有类型”
变量仍然有确定的静态类型,只是这个类型由编译器从初始化表达式推断出来
对字符串字面量:
1 | auto a = "hello"; // const char* |
如果希望得到 std::string,就要显式构造
不要只因为写了 auto 就以为它会推断成“最符合直觉”的类型
分支、循环和范围 for
1 | int score = 85; |
1 | for (int i = 0; i < 5; ++i) { |
C++ 中更推荐在合适场景下使用范围 for:
1 |
|
std::vector<int> 是一个动态数组,可以保存任意数量的 int 元素
for (int x : numbers) 的语义是:依次取出容器中的每个元素,复制一份给 x,然后执行循环体
如果元素很小,例如
int,复制成本几乎可以忽略;如果元素是一个很大的对象,每次循环都复制就可能很慢。这时可以用引用:
1
2
3 for (const auto& x : numbers) {
std::print("{} ", x);
}这表示不复制元素,而是用引用直接观察容器里的对象,并且
const承诺不会修改它
函数
函数把一段逻辑包装成一个可以反复调用的单元:
1 |
|
在这个例子中,r 的值会复制给参数 radius:
1 | 调用前:r = 5.0,存在 main 的栈帧中 |
因此,默认的值传递不会修改调用者的变量:
1 | void double_value(int x) { |
函数声明和定义
C++ 中,在调用一个函数之前,编译器必须已经知道它的签名:
1 | double square(double x); // 声明 |
函数声明告诉编译器:有这样一个函数,它叫什么,返回什么类型,参数是什么类型,函数定义提供完整实现
大型项目通常把声明放在头文件,把定义放在源文件,这样不同源文件可以共享接口,而不必互相复制实现
类和对象
类是 C++ 中组织数据和操作的核心机制
允许创建新的类型,把数据和操作这些数据的函数放在一起
1 |
|
类是蓝图,对象是按蓝图创建出来的实体:
1 | int main() { |
alice 和 bob 是同一个类的两个对象,但它们在内存中是两份独立的数据。修改 alice 的余额不会影响 bob
private、public 和约束
private 表示只能在类内部访问,外部代码不能直接读写:
1 | BankAccount alice; |
const 成员函数也是一种约束:
1 | double get_balance() const { |
函数末尾的 const 承诺:这个成员函数不会修改对象状态
编译器会检查这个承诺。如果在 const 成员函数里修改普通数据成员,编译器会报错
指针
每个变量都位于内存中的某个位置,这个位置可以用地址表示:
1 |
|
&x 表示 x 的地址。指针变量保存的就是地址:
1 | int x = 42; |
p 的类型是 int*,意思是“指向 int 的指针”
*p 是解引用,意思是沿着 p 里保存的地址找到那块内存,并按 int 的方式读写它
1 | *p = 100; |
内存视图如下:
1 | 修改前: |
改变的是 p 指向位置里的值,不是 p 本身保存的地址
引用
引用是已存在变量的别名:
1 | int x = 42; |
从语言语义上说,通过引用操作,就是直接操作被引用的对象
引用有两个约束:
- 声明时必须初始化
- 一旦绑定到某个对象,就不能改绑到另一个对象
参考下面这段代码:
1 | int a = 1; |
最后一行不是让 r 改为引用 b,而是把 b 的值赋给 a
执行后 a == 2,r 仍然是 a 的别名
指针和引用的区别
| 问题 | 指针 int* | 引用 int& |
|---|---|---|
| 可以为空吗 | 可以,常用 nullptr | 不可以,必须绑定到有效对象 |
| 可以改变指向吗 | 可以 | 不可以 |
| 使用时需要特殊符号吗 | 需要 *p 解引用 | 不需要,像普通变量一样用 |
| 声明时必须初始化吗 | 不一定 | 必须 |
引用常用于函数参数:
1 | void double_ref(int& x) { |
函数签名本身就是语义声明:
1 | void f(int x); // 我拿到的是副本,通常不会改你的原变量 |
const T&,它表示“我不想复制这个对象,也承诺不修改它”
标准库容器和字符串
std::vector 是动态数组:
1 |
|
当不断 push_back 时,std::vector 会根据需要在内部申请更大的连续内存,并在自身销毁时释放。使用者通常不需要手动 new 和 delete
std::string 是文本类型:
1 |
|
和 C 字符串相比,std::string 的优势是资源管理和语义更清楚:它知道自己的长度,支持拼接、比较、查找,也会自动管理内部内存
AI 时代怎么学 C++
- 多看:读成熟项目的代码,理解别人为什么这样组织类型、函数和接口
- 多编:代码必须经过编译、链接和运行,不是脑子里觉得对就对
- 多查:标准库和语言规则很多,不能只靠记忆,cppreference 应该放在优先位置
- 多问但要会质疑:AI 可以解释、改错、review,但它也会编造或遗漏细节
对于 C++ 尤其要避免只停留在“抽象语法”层面
C++ 代码最终会落到对象、内存、地址、拷贝、析构、函数调用这些具体行为上。专业训练的价值就在于能穿过语法表面,看见背后的成本和约束