Lec1 C++ 概述和 OOP 基础¶
1 C++ 基础¶
1. 1 I/O¶
C++ 采用流(stream)的方式处理输入输出,标准库提供了四个常见标准流对象:
std::cin:标准输入流,通常来自键盘std::cout:标准输出流,通常输出到终端std::cerr:标准错误流,通常无缓冲,适合立即输出错误信息std::clog:标准日志流,通常有缓冲,适合记录日志
示例:输入长度为 n 的数组并输出
pbfx/code/lec1/io.cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; i++) {
cin >> v[i];
}
for (int i = 0; i < n - 1; i++) {
cout << v[i] << " ";
}
cout << v.back() << endl;
return 0;
}
示例输入
5
1 2 3 4 5
运行结果
1 2 3 4 5
std::endl不只是换行,还会刷新缓冲区cin >> x会跳过前导空白,并在空格、制表符、换行处停止std::getline(std::cin, str)可以读取包含空格的一整行- 若需要控制输出格式,可使用
<iomanip>中的格式化工具
1. 2 命名空间¶
C 语言里常常靠变量名前缀规避命名冲突,而 C++ 用 namespace 专门解决这个问题。
示例
pbfx/code/lec1/namespace.cpp
#include <iostream>
using namespace std;
int value = 10;
namespace ThirdParty {
int value = 20;
}
int main()
{
cout << ThirdParty::value << endl;
cout << value << endl;
}
运行结果
20
10
使用规则
namespace Name { ... }定义命名空间::是作用域解析运算符,用于访问指定命名空间中的名字std是标准库所在的命名空间
using namespace std; 的利弊
- 平时写小程序时很方便
- 但它会把
std里的名字整体注入当前作用域,容易引发命名冲突 - 更稳妥的写法是显式写出
std::cout、std::vector这类限定名
1. 3 遍历¶
示例
pbfx/code/lec1/tranverse.cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> nums = {1, 2, 3, 4, 5};
for (int i = 0; i < (int)nums.size(); i++) {
cout << i << " "; // 下标遍历
}
cout << endl;
for (auto it = nums.begin(); it != nums.end(); ++it) {
cout << *it << " "; // 迭代器遍历
}
cout << endl;
for (auto num : nums) {
cout << num << " "; // 使用 for-range 遍历
}
return 0;
}
运行结果
0 1 2 3 4
1 2 3 4 5
1 2 3 4 5
解析
- 第一段输出的是下标
i,所以结果是0 1 2 3 4 - 第二段通过迭代器
it访问元素,*it得到当前元素值 - 第三段使用范围
for,语义最简洁,适合只读遍历
2 OOP 基础¶
什么是面向对象
面向对象(Object-Oriented)是一种编程范式。它把现实世界中的事物抽象成对象;相较于面向过程更关注“步骤怎么做”,面向对象更关注“由谁来做”。
2. 1 类和对象¶
- 类(Class):一种抽象数据类型,定义了一组属性(成员变量)和行为(成员函数)
- 对象(Object):类的实例(instance)
2. 2 封装¶
封装(Encapsulation)是把数据(属性)和操作这些数据的逻辑(方法)绑定在一起,并对外隐藏实现细节。
访问控制符
public:任何地方都可以访问protected:类内部和派生类可以访问private:仅类内部可以访问
不封装:C 风格栈
pbfx/code/lec1/stack_c.cpp
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct
{
int data[MAX_SIZE];
int top;
} Stack;
void initStack(Stack* s)
{
s->top = -1;
}
bool isEmpty(Stack* s)
{
return s->top == -1;
}
bool isFull(Stack* s)
{
return s->top == MAX_SIZE - 1;
}
void push(Stack* s, int value)
{
if (isFull(s)) {
printf("Stack overflow!\\n");
return;
}
s->data[++s->top] = value;
}
int pop(Stack* s)
{
if (isEmpty(s)) {
printf("Stack underflow!\\n");
return -1;
}
return s->data[s->top--];
}
特点
- 数据和操作是分离的
- 外部代码可以直接改
s.top或s.data - 需要把
Stack*显式传给每个函数
封装后:C++ 栈类
pbfx/code/lec1/stack.cpp
#include <iostream>
using namespace std;
const int MAX_SIZE = 100;
class Stack
{
private:
int data[MAX_SIZE];
int top;
public:
Stack() : top(-1) {};
bool isEmpty() const
{
return top == -1;
}
bool isFull() const
{
return top == MAX_SIZE - 1;
}
void push(int value)
{
if (isFull()) {
cout << "Stack overflow!" << endl;
return;
}
data[++top] = value;
}
int pop()
{
if (isEmpty()) {
cout << "Stack underflow!" << endl;
return -1;
}
return data[top--];
}
};
特点
data和top被藏在private中,外部不能直接乱改- 对外只暴露
push / pop / isEmpty / isFull这些接口 - 操作对象时写成
s.push(10),更符合“对象自己负责行为”的思路
两种栈实现的共同运行结果
stack.cpp 和 stack_c.cpp 都会完成 3 次压栈、4 次出栈。
stack.cpp 的实际输出是:
Popped value: 30
Popped value: 20
Popped value: 10
Popped value: Stack underflow!
-1
stack_c.cpp 的实际输出是:
Popped value: 30
Popped value: 20
Popped value: 10
Stack underflow!
Popped value: -1
为什么最后一行长得不一样
- C++ 版本里,
cout << "Popped value: " << s.pop() << endl;会先输出前缀,再在pop()内部输出Stack underflow! - C 版本里,
printf("Popped value: %d\n", pop(&s));中参数求值和输出的表现与当前实现组合后,最后形成了另一种打印顺序 - 这说明“把报错逻辑写在底层接口里”会影响外层输出格式,真实项目中应统一设计接口行为
C++ 中的 struct
- C++ 里同样有
struct - C++ 的
struct本质上也是类 - 与
class的核心区别只有一个: 未显式写访问控制符时,struct默认是public,class默认是private
类可以嵌套
类中还可以继续定义类或结构体。例如实现链表 class List 时,可以把节点 struct ListNode 定义在类内部。
为什么要封装
- 安全性:只能通过指定接口访问数据,减少意外修改
- 可维护性:代码职责更清晰
- 灵活性:内部实现可自由调整,而外部调用代码不必跟着改
课件最后的问题
C++ 的 private 主要是编译期访问控制,并不意味着对象内存从物理上“不可读”。
通过指针直接观察对象内存
pbfx/code/lec1/object.cpp
#include <iostream>
using namespace std;
class Example
{
private:
char c = 'A';
int i = 0x12345678;
char d = 'B';
};
int main()
{
Example e;
cout << "Size of Example: " << sizeof(e) << " bytes" << endl;
cout << "Hexadecimal representation:" << endl;
char* p = (char*)&e;
for (int i = 0; i < (int)sizeof(e); i++) {
printf("%02x", (p[i] & 0xFF));
}
printf("\\n");
return 0;
}
当前环境运行结果
Size of Example: 12 bytes
Hexadecimal representation:
417100007856341242710000
这段代码说明了什么
- 即使成员是
private,对象依然会以某种内存布局存放在内存中 - 若强行把对象地址转成字节指针,就有可能观察到底层字节
- 但这种做法高度依赖实现细节,不应作为正常编程手段
2. 3 内存对齐¶
为什么 Example 的大小是 12 字节
课件用下面的结构说明:对象成员虽然通常按声明顺序排布,但会受到内存对齐(alignment)影响。
struct Example {
char c; // 1 字节
int i; // 4 字节
char d; // 1 字节
};
在当前环境中的一种典型布局
c占 1 字节- 为了让
int i按 4 字节对齐,c后面会补若干填充字节 i占 4 字节,且在小端机器上会表现为78 56 34 12d占 1 字节- 结构体尾部还可能继续补齐,使整体大小成为对齐单位的整数倍
关于输出中的那些额外字节
上面十六进制结果里除了 41('A')、78 56 34 12、42('B')以外,其余字节主要反映了当前环境下的填充与对象表示;这些字节值本身不应被当作稳定语义去依赖。