C++学习笔记——箭头运算符、std::vector的使用、静态链接、动态链接

目录

[1. 箭头运算符](#1. 箭头运算符)

[1.1 基本用法](#1.1 基本用法)

[1.2 箭头运算符的重载](#1.2 箭头运算符的重载)

[1.3 箭头运算符与偏移量](#1.3 箭头运算符与偏移量)

[2. std::vector 动态数组](#2. std::vector 动态数组)

[2.1 std::vector 的基本工作原理](#2.1 std::vector 的基本工作原理)

[2.2 优化std::vector在C++中的使用](#2.2 优化std::vector在C++中的使用)

[2.3 总结](#2.3 总结)

[3. 静态链接与动态链接](#3. 静态链接与动态链接)

[3.1 为什么 C++ 的库管理更棘手?](#3.1 为什么 C++ 的库管理更棘手?)

[3.2 什么是静态链接?](#3.2 什么是静态链接?)

[3.3 什么是动态链接?](#3.3 什么是动态链接?)

[3.4 静态链接 vs 动态链接](#3.4 静态链接 vs 动态链接)

[3.5 如何配置静态链接(以 GLFW 库为例)](#3.5 如何配置静态链接(以 GLFW 库为例))

[3.6 如何配置静态链接(以 GLFW 库为例)](#3.6 如何配置静态链接(以 GLFW 库为例))


1. 箭头运算符

1.1 基本用法

  • 对于指针类型-> 运算符用于访问指针所指向对象的成员(变量或函数)。

  • 它等价于先解引用再访问:ptr->member 等同于 (*ptr).member

  • 主要作用:简化语法,避免显式写 * 和括号。

1.2 箭头运算符的重载

为什么需要重载?

  • 自定义类(如智能指针、迭代器)希望表现得像指针一样,让用户通过 -> 访问其管理的对象。

  • 重载 -> 必须返回一个指针另一个重载了 -> 的类对象(编译器会递归调用,直到得到原始指针)。

cpp 复制代码
class SmartPointer {
private:
    Entity* m_Ptr;
public:
    SmartPointer(Entity* ptr) : m_Ptr(ptr) {}
    Entity* operator->() const {   // 返回原始指针
        return m_Ptr;
    }
};

SmartPointer sp(new Entity{5});
sp->Print();   // 调用 operator-> 返回 Entity*,然后调用Entity的成员方法Print()

支持链式重载:

cpp 复制代码
class Proxy {
    Entity* m_Ptr;
public:
    Proxy(Entity* ptr) : m_Ptr(ptr) {}
    Entity* operator->() { return m_Ptr; }
};

class DoubleProxy {
    Proxy m_Proxy;
public:
    DoubleProxy(Entity* ptr) : m_Proxy(ptr) {}
    Proxy operator->() { return m_Proxy; }
};

DoubleProxy dp(new Entity{42});
dp->Print();   // 先调用 DoubleProxy::operator-> 返回 Proxy,
               // 再调用 Proxy::operator-> 返回 Entity*,最后调用 Print()

1.3 箭头运算符与偏移量

利用空指针计算成员偏移量:

cpp 复制代码
struct Vector2 {
    float x, y;
};

int offset = (int)&(((Vector2*)nullptr)->y);
  • (Vector2*)nullptr 创建一个指向地址 0Vector2* 指针(空指针)。

  • ->y 试图访问该指针的 y 成员。虽然空指针解引用在运行时是未定义行为,但在编译时 ,编译器只需要计算出 y 相对于结构体起始地址的偏移量(offset),而不会实际访问内存。

  • &(((Vector2*)nullptr)->y)y 的地址。由于基地址是 0,该地址的数值就等于 y 在结构体中的偏移字节数。

  • 最后强制转换为 int 得到偏移量。

2. std::vector 动态数组

2.1 std::vector 的基本工作原理

  • std::vector 是一个动态数组,在堆上连续存储元素。

  • 它维护三个指针(或大小/容量):

    • 大小(size):当前实际元素个数。

    • 容量(capacity):已分配内存能容纳的元素个数。

    • 数据指针(data):指向堆内存的起始位置。

  • size == capacity 时,再次添加元素会触发重新分配

    1. 分配一块新的更大的内存。

    2. 将旧元素拷贝(或移动)到新内存。

    3. 销毁旧内存上的元素并释放原内存。

2.2 优化std::vector在C++中的使用

拷贝开销来源

1.push_back 中的临时对象拷贝

cpp 复制代码
vertices.push_back({1,2,3});   // {1,2,3} 是临时 Vertex,先构造,再拷贝到 vector 内部
  • 步骤:构造临时 Vertex → 拷贝构造到 vector 内部 → 销毁临时对象。

  • 进行一次拷贝和一次销毁。

2.容量不足导致的重新分配与拷贝

cpp 复制代码
vertices.push_back({2,3,4});   // 容量不足,触发重新分配
  • 旧元素被逐一拷贝到新内存,然后逐一析构。当元素是复杂对象时,开销巨大。

优化策略

1.使用 reserve() 预分配容量

cpp 复制代码
vertices.reserve(3);   // 一次性分配至少能容纳 3 个元素的内存
  • 避免多次重新分配和拷贝。

  • 应在填充元素前调用,且只需分配一次足够大的容量。

2. 使用 emplace_back() 代替 push_back()

cpp 复制代码
vertices.emplace_back(1,2,3);   // 直接在 vector 内存中构造 Vertex,无需临时对象
  • emplace_back 接受构造参数,在 vector 的尾部内存中直接构造对象,零拷贝

  • 相比 push_back({1,2,3}),省去了一次临时对象拷贝和销毁。

3. 避免不必要的传值拷贝(函数参数)

cpp 复制代码
void Clear1(std::vector<Vertex> vertices) { vertices.clear(); }   // 拷贝整个 vector
void Clear2(std::vector<Vertex>& vertices) { vertices.clear(); }   // 引用,无拷贝
  • 示例所示,Clear1 不会影响原始 vector,因为传参时发生了深拷贝。

  • 只读操作传 const std::vector<T>&;修改操作传 std::vector<T>&

2.3 总结

场景 推荐做法
已知元素数量 使用 reserve() 预分配容量
插入元素 优先使用 emplace_back() 而非 push_back()
函数传参 需要修改传引用,只读传 const&
避免不必要的拷贝 遍历用 const auto&,避免按值传递容器

3. 静态链接与动态链接

3.1 为什么 C++ 的库管理更棘手?

C++ 没有像 Python、C#、Java 那样的包管理器,无法简单地通过一条命令(如 import)来引入库。不同平台、不同配置(Debug/Release)、不同架构(32位/64位)都需要匹配对应的库文件。

3.2 什么是静态链接?

静态链接 是在编译、链接阶段 完成所有符号解析和地址重定位的过程。链接器会将程序所依赖的静态库.lib.a 文件)中的目标代码直接拷贝并合并 到最终的可执行文件(.exe.out)中。

3.3 什么是动态链接?

动态链接 将链接过程推迟到程序运行时 。可执行文件中只记录所需动态库.dll / .so / .dylib)的名称及需要的符号信息,而不包含库的机器码。在程序加载或执行过程中,由动态链接器(dynamic linker / loader)负责找到、加载、链接这些库。

3.4 静态链接 vs 动态链接

特性 静态链接 动态链接
库文件位置 嵌入 .exe 内部 运行时作为独立 .dll(Windows)或 .so(Linux)文件存在
可执行文件大小 较大 较小
运行时依赖 无,独立运行 需要 .dll 文件位于合适位置
启动速度 更快(无需加载外部库) 稍慢(运行时加载)
更新库 需重新编译整个程序 只需替换 .dll 文件
编译器优化 可执行链接时优化(LTO),性能更佳 优化受限
调试体验 完整符号信息可用 相对复杂

3.5 如何配置静态链接(以 GLFW 库为例)

步骤 1:下载库文件

从 GLFW 官网下载预编译的二进制文件,需根据目标应用程序的架构选择 32 位或 64 位版本(而非根据当前操作系统)。解压后,库通常包含两个目录:

  • include/:头文件(声明函数接口)

  • lib/ :预编译的二进制库文件(.lib.dll

步骤 2:配置包含目录(Include Directories)

在 Visual Studio 项目属性中,找到 C/C++ → 常规 → 附加包含目录 ,添加 include 文件夹的路径。建议使用 $(SolutionDir) 来表示解决方案目录,这样使用相对路径可以避免因项目移动而导致路径失效的问题。

配置完成后,就可以在代码中包含头文件了:

cpp 复制代码
#include <glfw3.h>

步骤 3:配置库目录(Library Directories)

在项目属性中,找到 链接器 → 常规 → 附加库目录 ,添加 lib 文件夹的路径

步骤 4:指定要链接的库文件

链接器 → 输入 → 附加依赖项 中,添加库文件名(如 glfw3.lib)。需要注意,必须包含 .lib 后缀 ,否则链接器可能会尝试打开 .obj 文件导致找不到文件-

3.6 如何配置静态链接(以 GLFW 库为例)

步骤 1:下载动态库版本

下载 GLFW 预编译包时,选择动态库版本(通常文件名含 dll 字样)。解压后得到:

  • include ------ 头文件(与静态版相同)

  • lib ------ 导入库文件 glfw3dll.lib(用于编译链接,很小,只负责引导)

  • glfw3.dll ------ 真正的动态库(运行时需要)

步骤 2:配置包含目录 (与静态相同)
C/C++ → 附加包含目录 指向 include 文件夹

步骤 3:配置库目录
链接器 → 附加库目录 指向存放 glfw3dll.lib 的文件夹。

步骤 4:指定导入库
链接器 → 附加依赖项 添加 glfw3dll.lib(注意文件名与静态库不同)。

步骤 5:部署 DLL 文件

生成 exe 后,必须将 glfw3.dll 放在 exe 同一目录 ,或放在系统能搜索到的路径(如 C:\Windows\System32,不推荐)。否则运行时会报"找不到 glfw3.dll"错误。

相关推荐
ZhiqianXia2 小时前
Pytorch 学习笔记(17):decompositions.py —— 算子分解的百科全书
pytorch·笔记·学习
xian_wwq2 小时前
【学习笔记】大模型如何理解图片
笔记·学习
郭涤生2 小时前
原子操作的内存顺序
c++
ALex_zry2 小时前
C++模板元编程实战技巧
网络·c++·windows
talen_hx2962 小时前
《零基础入门Spark》学习笔记 Day 13
笔记·学习·spark
Flittly2 小时前
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
java·笔记·spring·ai·springboot
SteveSenna2 小时前
强化学习4.1:基于价值——Q-learning
人工智能·学习·算法·机器人
少许极端2 小时前
算法奇妙屋(四十四)-贪心算法学习之路11
java·学习·算法·贪心算法
ambition202422 小时前
斐波那契取模问题的深入分析:为什么提前取模是关键的
c语言·数据结构·c++·算法·图论