Skip to content

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::coutstd::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.tops.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--];
    }
};
特点
  • datatop 被藏在 private 中,外部不能直接乱改
  • 对外只暴露 push / pop / isEmpty / isFull 这些接口
  • 操作对象时写成 s.push(10),更符合“对象自己负责行为”的思路
两种栈实现的共同运行结果

stack.cppstack_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 默认是 publicclass 默认是 private

类可以嵌套

类中还可以继续定义类或结构体。例如实现链表 class List 时,可以把节点 struct ListNode 定义在类内部。

为什么要封装
  1. 安全性:只能通过指定接口访问数据,减少意外修改
  2. 可维护性:代码职责更清晰
  3. 灵活性:内部实现可自由调整,而外部调用代码不必跟着改
课件最后的问题

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 12
  • d 占 1 字节
  • 结构体尾部还可能继续补齐,使整体大小成为对齐单位的整数倍

关于输出中的那些额外字节

上面十六进制结果里除了 41'A')、78 56 34 1242'B')以外,其余字节主要反映了当前环境下的填充与对象表示;这些字节值本身不应被当作稳定语义去依赖。