hugo

docys

Todo

Todo

在使用 docys 过程中一些需要处理的问题

List

  1. 修改 Homepage
  2. 如何设置一个文件夹继承 “docs”, “blog” 等页面属性

randomization

真随机数 (True random number) 和假随机数 (False random number)

KISS Random

Keep It Simple Stupid(KISS)

简介

KISS(“Keep It Simple Stupid”)是一种伪随机数生成器(PRNG)家族,由George Marsaglia引入。KISS生成器通过结合多个独立的随机数生成器来提高随机数的质量。早期版本包括一个线性同余生成器和两个线性反馈移位寄存器生成器,而后来的版本引入了乘-携带生成器和Xorshift生成器。尽管KISS生成器在一些统计测试中表现良好,但它并不适合加密用途​

code

迭代和递归

正文

迭代和递归, 一直是编程中的核心概念, 他们涉及到算法中的查询, 插入, 删除等各种操作, 也预示着编程的重要思想, “可重复性” 和 “分步性”

python

Plotly Python 速查

绘制线段

快速可视化工具

快速查看平面几何图形

Plotly 优点:Plotly 是一个非常强大的交互式绘图库。它允许你创建交互式图形,适合在网页或数据分析报告中嵌入图表。即使是简单的图形,它也能通过非常直观的 API 快速实现。

如何理解 python 中的 module

1.1 什么是 module

module 是一个包含 Python 代码的文件,它可以定义函数、类、变量,也可以包含可执 行的代码。模块是 Python 代码的组织单位,有助于将代码分割成更小、更可管理的部分。 换而言之, Python 中的任意 .py 文件都可以作为模块(module)来使用。

1.2 创建 module

一个模块就是一个 Python 文件(.py 文件),文件名即为模块名。例如,创建一个名为 my_module.py 的文件:

# my_module.py

def greet(name):
    return f"Hello, {name}!"

PI = 3.14159

1.3 导入模块并使用

在另一个 Python 文件或交互式环境中,可以使用 import 语句导入模块:

# main.py

import my_module

print(my_module.greet("Alice"))  # 输出: Hello, Alice!
print(my_module.PI)              # 输出: 3.14159

1.0 模块的结构和组织 — 包(packages)

包是模块的集合,实际上是包含多个模块的目录(一个包含一些.py 文件的 folder)。包目 录中必须包含一个 __init__.py 文件(即使它为空),表示这是一个包。

# directory structure
my_package/
    __init__.py
    module1.py
    module2.py

1.1 ‘init.py’ 文件

__init__.py 文件用于初始化包。它可以包含初始化代码,或者用来定义包的公共接口 。Python 3.3 及以后的版本中,init.py 文件不是必须的,但它仍然被广泛使用

1.2 ‘init.py’ 的功能

1.2.1 空的 init.py 文件

如果你只是想将目录标记为一个包,没有其他需求,可以将 init.py 文件保持为空。

# __init__.py

1.2.2 初始化代码

如果你需要在包被导入时执行一些初始化代码,可以在 init.py 文件中编写这些代码 。例如:

# __init__.py
print("Initializing the package")

# 初始化代码
def initialize():
    print("Package has been initialized")

1.2.3 定义包的公共接口

可以在 __init__.py 文件中导入包内的重要模块或函数,以便用户在导入包时能直接访 问这些功能。例如:

# __init__.py
from .moduleA import functionA
from .moduleB import ClassB

__all__ = ['functionA', 'ClassB']

这里的 all 列表用于定义 from package import * 时要导入的模块或对象。

1.2.4 将当前目录临时加入到 pythonpath 中

当 module1 试图引用 module2 时, 可能会报错 “No module named ‘module2’”

import os
import sys

current_dir = os.path.dirname(__file__)

if current_dir not in sys.path:
    sys.path.append(current_dir)

print("Init", __file__)

Python 虚拟环境

什么是 python 的虚拟环境

Python 虚拟环境是 Python 开发中的一种工具,用于创建和管理独立于系统全局 Python 环境的隔离环境。每个虚拟环境都可以拥有自己的 Python 解释器和独立的包集合,这使得在同一台机器上同时进行多个项目开发时能够保持环境的干净和隔离。

为什么需要 python 的虚拟环境

  1. 隔离依赖:每个虚拟环境都有自己的 site-packages 目录,用于存放安装的第三方包,这样可以避免不同项目之间的包冲突。

  2. 版本管理:可以在不同的虚拟环境中使用不同的 Python 版本,或者使用同一 Python 版本的不同补丁版本。

  3. 便于维护:虚拟环境使得项目的依赖和配置可以被单独管理和复制,方便分享和部署到其他环境。

  4. 环境复制:通过共享 requirements.txt 或者 environment.yml 文件,可以快速重建相同的开发环境,确保不同开发者之间开发环境的一致性。

创建&管理 python 虚拟环境的方法和工具

  • Python 内置的 venv 模块
  • 第三方工具 virtualenv

python 虚拟环境的作用范围与生命周期

每个虚拟环境的激活仅限于它所在的命令行窗口或应用程序范围内,不会影响系统的全局设置或其他窗口中的环境, 所以,当关闭激活虚拟环境的命令行窗口或终端后,该虚拟环境就会自动关闭。这是因为虚拟环境的激活状态仅在当前的命令行会话中有效,一旦会话结束(即窗口关闭),虚拟环境的激活状态也随之结束。

关闭窗口后,再次打开新窗口时,默认情况下系统将使用全局 Python 环境,除非你手动再次激活某个虚拟环境。虚拟环境的激活和关闭是轻量级的操作,设计上不会对系统产生持久影响。

c++笔记

描述

C++ 是一种广泛使用的编程语言,其标准由国际标准化组织(ISO)维护。C++ 标准的发展 经历了多个版本,每个版本都引入了新的特性和改进。以下是 C++ 标准的主要版本及其主 要特性:

主要版本

  1. C++98 (ISO/IEC 14882:1998)

    • 第一个正式的 C++ 标准。
    • 引入了标准模板库(STL),包括容器、算法和迭代器等。
    • 支持模板、异常处理和运行时类型识别(RTTI)。
  2. C++03 (ISO/IEC 14882:2003)

    • 对 C++98 的小修小补,主要是修复了一些已知的问题。
    • 提供了更好的库支持和一些语言细节的改进。
  3. C++11 (ISO/IEC 14882:2011)

    • 引入了许多重要的新特性,大大增强了语言的功能和表达能力。
    • 主要特性包括:
      • 自动类型推导 (auto 和 decltype)
      • lambda 表达式
      • 右值引用 和 移动语义
      • 智能指针 (std::unique_ptr, std::shared_ptr)
      • 多线程支持 (std::thread, std::mutex)
      • 范围 for 循环
      • nullptr
      • 静态断言 (static_assert)
  4. C++14 (ISO/IEC 14882:2014)

    • 对 C++11 的进一步改进,增加了更多的便利性和功能。
    • 主要特性包括:
      • 泛型 lambda 表达式
      • 返回类型推导 (auto 函数)
      • 变量模板
      • 二进制字面量
      • 数字分隔符 (')
  5. C++17 (ISO/IEC 14882:2017)

    • 继续改进 C++14 的特性,并引入了一些新的特性。
    • 主要特性包括:
      • 结构化绑定 (auto [x, y] = f();)
      • if 和 switch 语句中的初始化 (if (int x = f(); x > 0))
      • 文件系统库 (<filesystem>)
      • 折叠表达式 (用于模板参数包)
      • 内联变量
      • constexpr 改进
  6. C++20 (ISO/IEC 14882:2020)

    • 引入了许多现代化的语言特性和库。
    • 主要特性包括:
      • 概念 (concept 关键字,用于模板约束)
      • 范围 (std::ranges 库)
      • 协程 (std::coroutine)
      • 模块 (module 关键字,用于替代头文件)
      • 日历和时间 zone 库
      • 三重引号字符串字面量
      • 模板参数列表中的 auto
  7. C++23 (ISO/IEC 14882:2023)

    • 最近发布的一个版本,继续扩展和完善 C++。
    • 主要特性包括:
      • 标准化的协程 (std::coroutine 的进一步改进)
      • 改进的范围库 (std::views 的扩展)
      • 标准化的格式化库 (std::format)
      • 标准化的 span 类型 (std::span)
      • 改进的模块支持
      • 标准化的预期类型 (std::expected)

编译器支持

不同的编译器对这些标准的支持程度不同。以下是一些常见编译器的最新版本对 C++ 标准 的支持情况:

  • GCC (GNU Compiler Collection):从 GCC 4.8 开始支持 C++11,GCC 7 开始支持 C++17,GCC 10 开始支持 C++20,GCC 13 开始支持 C++23。
  • Clang:从 Clang 3.1 开始支持 C++11,Clang 5 开始支持 C++17,Clang 10 开始 支持 C++20,Clang 16 开始支持 C++23。
  • Microsoft Visual Studio:从 VS 2015 开始支持 C++11,VS 2017 开始支持 C++17,VS 2019 开始支持 C++20,VS 2022 开始支持 C++23。

include 的细节

#include “headername” 和 #include <headername> 的区别

#include "headername" 和 #include <headername> 的主要区别在于头文件的搜索路 径。虽然在包含标准库文件(如 iostream)时,两者的行为通常没有区别,但在实际开发 中,两者有不同的语义,尤其是在包含自定义头文件时。

1.#include “headername”

  • 含义:首先在当前目录(即源文件所在的目录)中搜索头文件。如果没有找到该头文件, 编译器会在标准的系统路径中继续查找。

  • 用法:这种方式常用于包含用户自定义的头文件(非标准库头文件)。

  • 搜索路径:

    1. 当前源文件所在目录。
    2. 如果在当前目录找不到头文件,编译器会在标准的包含路径(如 /usr/include/ 或 C:\Program Files...)中继续查找

2.#include <headername>

  • 含义:编译器会直接在系统的标准路径中搜索头文件,而不检查当前目录。

  • 用法:这种方式通常用于包含标准库头文件,例如 iostream、vector 等。

  • 搜索路径:直接在系统的标准头文件路径中查找,不查找当前目录。

区别

  1. 主要区别是搜索路径:
    • #include "headername":先查找当前目录,然后再查找系统标准目录。
    • #include <headername>:只查找系统标准目录,不查找当前目录。
  2. 使用场景:
    • #include "headername":主要用于用户自定义的头文件,开发者可以将头文件放在 当前项目目录中。
    • #include <headername>:主要用于标准库头文件或系统级头文件,编译器会从系统 的标准路径中查找。

对于标准库头文件(如 #include <iostream>),两者的区别在很多编译器中不大,因 为即使你使用 #include "iostream",编译器也会在标准路径中找到标准库头文件。但 是,规范上推荐使用 #include <iostream> 来包含标准库头文件,因为它更明确且可 以避免潜在的冲突。

pragma 指令

描述

在 C++ 中,#pragma 是一种编译器指令,用于向编译器提供特定的编译指示。它并不是 C++ 标准的一部分,而是由不同编译器实现的,因此它的作用和支持的指令可能会因编译器 的不同而有所差异。

#pragma 是一种编译器指令,用于执行特定的编译器行为,常用于内存对齐、警告控制、优 化等任务。由于它不是 C++ 标准的一部分,因此其使用和功能通常依赖于具体的编译器。 在跨平台开发时需要特别注意,尽量避免编译器特定的 #pragma 指令,以提高代码的可移 植性

1. #pragma once

  • 用于防止头文件被多次包含。
  • 它告诉编译器,在编译过程中只包含一次该文件的内容。相比传统的 include guard (#ifndef、#define、#endif),它更简洁。

示例

#pragma once
// 头文件内容

等同于

#ifndef HEADER_FILE_NAME
#define HEADER_FILE_NAME
// 头文件内容
#endif

2. #pragma pack

  • 用于修改结构体或类的字节对齐方式。通常编译器会根据平台的要求为结构体字段进行内 存对齐,但通过 #pragma pack,你可以改变默认的对齐方式

下面的示例会让 MyStruct 使用 1 字节对齐,避免结构体字段之间的填充字节。

示例

#pragma pack(1) // 设置为1字节对齐
struct MyStruct {
    char a;
    int b;
};
#pragma pack()  // 恢复默认对齐

3. #pragma warning

  • 用于控制编译过程中是否显示某些警告,通常是在禁用特定警告或启用更多警告信息时使 用。

示例

#pragma warning(push)       // 保存当前警告状态
#pragma warning(disable:4996) // 禁用某个警告(如过时的函数使用)

// 这里可以包含会触发该警告的代码,但警告会被忽略
#pragma warning(pop)        // 恢复之前的警告状态

4. #pragma GCC(GCC 编译器专有)

  • 针对 GCC 编译器的 #pragma,如 #pragma GCC optimize,用于控制优化级别。

示例

#pragma GCC optimize ("O2") // 使用O2优化级别

5. #pragma region / #pragma endregion

  • 这个指令用于组织代码的区域,尤其在 IDE(如 Visual Studio)中,有助于折叠和展开 代码段。它主要用于改善代码可读性和导航

示例

#pragma region MyRegion
// 该区域内的代码可以被折叠
int a = 10;
int b = 20;
#pragma endregion MyRegion

C 动态分配二维数组的内存

指针数组

使用指针数组, 每个指针指向一维数组的起始位置

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配行指针数组
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 为每一行分配内存
    for (int i = 0; i < rows; i++) {
        array[i] = (double *)malloc(cols * sizeof(double));
        if (array[i] == NULL) {
            printf("Memory allocation failed\n");
            // 释放已分配的内存
            for (int j = 0; j < i; j++) {
                free(array[j]);
            }
            free(array);
            return 1;
        }
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);

    return 0;
}

连续内存块

分配一个连续的内存块,并使用指针数组管理。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配行指针数组
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 分配一个连续的内存块
    double *data = (double *)malloc(rows * cols * sizeof(double));
    if (data == NULL) {
        printf("Memory allocation failed\n");
        free(array);
        return 1;
    }

    // 使每个行指针指向连续内存块的对应位置
    for (int i = 0; i < rows; i++) {
        array[i] = data + i * cols;
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    free(data);
    free(array);

    return 0;
}

指向指针的指针

分配一个指向指针的指针,然后分配每一行的内存.

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;

    // 分配指向指针的指针
    double **array = (double **)malloc(rows * sizeof(double *));
    if (array == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 为每一行分配内存
    for (int i = 0; i < rows; i++) {
        array[i] = (double *)malloc(cols * sizeof(double));
        if (array[i] == NULL) {
            printf("Memory allocation failed\n");
            // 释放已分配的内存
            for (int j = 0; j < i; j++) {
                free(array[j]);
            }
            free(array);
            return 1;
        }
    }

    // 初始化并打印二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
            printf("%f ", array[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);

    return 0;
}

总结

  • 方法一:适合简单的二维数组分配,但每行的内存不一定是连续的。
  • 方法二:内存是连续的,适合需要频繁访问二维数组元素的情况,性能较好。
  • 方法三:类似于方法一,但更灵活,可以用于更复杂的结构。

在 C 函数中建议 move data to heap 时

在 C 语言中,将数据移动到堆内存(heap)上,可以通过动态内存分配函数来实现。这些函数包括 malloc、calloc、realloc

malloc

malloc(memory allocation)函数用于分配指定字节数的内存,并返回指向这块内存的指针。内存未初始化。

#include <stdlib.h>

int main() {
    // 分配内存用于存储10个整数
    int *array = (int *)malloc(10 * sizeof(int));
    if (array == NULL) {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // 释放内存
    free(array);

    return 0;
}

calloc

calloc(contiguous allocation) 函数用于分配指定数量的元素, 每个元素大小为指定字节数, 并将分配的内存初始化为 zero;

#include <stdlib.h>
int main()
{
    // 分配内存用于存储 10 个整数, 并初始化为 zero
    int *array = (int*)calloc(10, sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用分配的内存
    for(int i = 0; i < 10; i++>)
    {
        // array[i] 已初始化为 0
    }

    // 释放内存
    free(array);

    return 0;
}

realloc

realloc (re-allocation) 函数用于调整之前分配的内存块的大小, 如果新的内存大于旧的内存, 新的内存部分未初始化

#include <stdlib.h>

int main()
{
    // 分配内存用于存储 10 个整数
    int *array = (int *)malloc(10 * sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 调整 内存块的大小以存储 20 个整数
    array = (int *)realloc(array, 20 * sizeof(int));
    if(array == NULL)
    {
        // 处理内存分配失败的情况
        return 1;
    }

    // 使用调整后的内存
    for(int i = 0; i < 20; i++)
    {
        array[i] = i;
    }

    // 释放内存
    free(array);

    return 0;
}

Note: 注意事项

  1. 内存泄漏:确保每个通过 malloc、calloc 或 realloc 分配的内存块最终都被 free 释放,否则会导致内存泄漏。
  2. 空指针检查:在使用分配的内存之前,始终检查返回的指针是否为 NULL,以确保内存分配成功。
  3. 内存初始化:使用 calloc 可以确保分配的内存被初始化为零,而 malloc 分配的内存则未初始化。

比较 boost 和 stl

Boost 和 STL(Standard Template Library)是两个非常重要的 C++ 库,它们各自提供了 丰富的功能和工具,但在用途、设计哲学和使用场景上有一些显著的区别。下面是对 Boost 和 STL 的详细比较:

STL(Standard Template Library)

  1. 标准库的一部分:STL 是 C++ 标准库的一部分,由 ISO/IEC 标准化组织定义。所 有符合标准的 C++ 编译器都必须提供 STL。
  2. 核心组件:STL 主要包含容器、算法和迭代器三个核心组件。
    • 容器:如 vector、list、map、set 等,用于存储和管理数据。
    • 算法:如 sort、find、copy 等,用于对容器中的数据进行操作。
    • 迭代器:用于遍历容器中的元素,提供了统一的接口。
  3. 轻量级:STL 设计简洁,专注于基本的数据结构和算法,适合大多数常见的编程任 务。
  4. 性能优化:STL 的实现经过高度优化,性能通常非常好。

使用场景

  • 通用数据结构和算法:适用于大多数常见的数据管理和操作需求。
  • 学习和教学:STL 是学习 C++ 标准库和模板编程的好起点。

Boost

  1. 第三方库:Boost 是一个第三方库集合,由 C++ 社区开发和维护,不是标准库的一 部分。
  2. 功能丰富:Boost 提供了比 STL 更多的功能,涵盖了网络编程、线程管理、正则表 达式、智能指针、日期和时间处理等多个领域。
  3. 模块化:Boost 包含许多独立的模块,每个模块解决特定的问题。用户可以根据需 要选择和使用这些模块。
  4. 高级特性:Boost 提供了许多高级特性,如元编程、函数对象、信号槽机制等,适 合复杂的编程任务。
  5. 社区支持:Boost 有一个活跃的社区,提供了丰富的文档、示例和讨论论坛。

使用场景

  • 复杂应用开发:适用于需要高级功能和复杂数据结构的项目。
  • 高性能计算:许多 Boost 模块针对高性能计算进行了优化。
  • 跨平台开发:Boost 提供了许多跨平台的解决方案,适合多平台项目。

对比总结

功能对比

  • STL:主要提供基本的数据结构和算法,适合大多数常见的编程任务。
  • Boost:提供更丰富的功能和高级特性,适合复杂的编程需求。

性能对比

  • STL:经过高度优化,性能通常非常好。
  • Boost:某些模块也经过优化,但整体性能取决于具体的模块和实现。

易用性对比

  • STL:设计简洁,易于学习和使用。
  • Boost:功能丰富,但可能需要更多的时间来学习和掌握。

社区和支持

  • STL:作为标准库的一部分,有广泛的文档和教程支持。
  • Boost:有活跃的社区,提供了丰富的资源和支持。

结论

  • STL 是 C++ 标准库的一部分,提供了基本的数据结构和算法,适合大多数常见的编 程任务。
  • Boost 是一个功能丰富的第三方库,提供了许多高级特性,适合复杂的编程需求。

move函数的实现原理

C++ 是如何实现面向对象编程的

面向对象的三个特性

  • 封装:将对象的状态(数据成员)和行为(成员函数)捆绑在一起,并控制对它们的访问。封装的主要目的是隐藏类的内部实现细节,并仅暴露必要的接口供外部使用。这样可以保护数据,防止外部代码意外或恶意地改变对象的状态。
    • 数据成员可以被声明为 private、protected 或 public。
    • 成员函数通常也是 public 的,以便用户可以调用这些函数来操作对象。
    • 使用 private 和 protected 修饰符可以限制对数据成员的直接访问。
  • 继承:允许创建新的类(称为派生类或子类)从现有的类(称为基类或父类)继承属性和方法。这样可以复用代码,同时也可以扩展或修改基类的行为。
    • 派生类可以继承基类的所有非私有成员。
    • 派生类可以添加新的成员变量和方法,或者重写基类的方法。
    • 继承支持层次化的分类结构,便于建模现实世界的关系
  • 多态: 允许使用一个接口来表示多种类型的特性。在 C++ 中,多态通常是通过 虚函数 (virtual function)来实现的。当一个函数被声明为虚函数时,可以在派生类中重写该函数,并且可以通过基类的指针或引用调用派生类中的重写版本。
    • 支持运行时多态性(runtime polymorphism),即可以在运行时确定调用哪个版本的函数。
    • 通常与虚函数和接口有关

虚函数 和 纯虚函数

虚函数的实现机制

C++ 中多态的实现原理

多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。

重载 | 重写 | 隐藏

  • 函数重载: 同一可访问区 内被声明几个具有 不同 参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
  • 函数重写(覆盖): 派生类中存在 重新定义 的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数 必须 有 virtual 修饰。
  • 函数隐藏: 在面向对象编程中,特别是在 C++ 中,当一个派生类(derived class)中定义了一个与基类(base class)中相同名称的成员函数时,如果没有特别标记为 virtual,那么派生类中的成员函数就会“隐藏”掉基类中的同名成员函数。这意味着当你通过派生类的对象调用这个函数时,调用的是派生类中的版本,而不是基类中的版本。
class Base
{
public:
    void fun(int tmp, bool tag ) { cout << "Base::fun(int tmp, bool tmp1)" << endl; }
    virtual void vir_fun(int* array) { cout << "Base::vir_fun(int* array)" << endl; }
}

class Derived: public Base
{
public:
    void fun(int tmp);              // 隐藏基类中的同名函数
    void fun(float tmp);            // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1);  // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1);  // 重载 参数顺序不同(相对于上一个函数)
    int  fun(int tmp);               // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型

    virtual void vir_fun(int* array) { cout << "Derived::vir_fun(int* array)"<< endl; }
};

int main()
{

    Derived ex;
    ex.fun(1);         // Derive::fun(int tmp)
    ex.fun(1, false);  // error: candidate expects 1 argument, 2 provided
    ex.fun.Base.fun(1, true); // Base::fun(int bool)

    Base *base = new Derived();
    base->vir_fun({1,3,4}); // Derived::vir_fun(int* array)

    return 0;
}

重载和重写的区别

  • 范围区别:对于作用函数的 空间 而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
  • 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都必须和原函数相同,父类中被重写的函数需要有 virtual 修饰。
  • virtual:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有(取决于重载的函数是否被派生)。

隐藏和重写, 重载的区别

  • 范围区别:隐藏与重载 范围不同,隐藏发生在不同类中。
  • 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,只要派生类中有一个同名函数, 基类函数都是被隐藏,而不是重写。
  • 利用重写可以实现多态,而隐藏不可以: 如果使用基类指针 p 指向派生类对象,利用这个指针调用函数时,对于隐藏的函数,会根据指针的类型去调用函数;对于重写的函数,会根据指针所指对象的类型去调用函数。重写必须使用 virtual 关键字,此时会更改派生类虚函数表的表项。
  • 隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口

sizeof 和 strlen 的区别

编译与链接

什么是编译

C++ 是一种静态类型的编程语言,CPU 无法直接执行文件中的各行命令, 这意味着在程序执行之前需要通过 编译过程 将其源代码转换为机器码. 编译器会读取程序的源文件 .cpp, 将其翻译为可执行文件 ELF, ELF 文件可以经过操作系统进行加载执行

编译的过程

常见的编译过程分为四个过程: 编译预处理, 编译, 汇编, 链接

  • 编译预处理:在预编译过程中主要处理源代码中的预处理指令,比如引入头文件#include,去除注释,处理所有的条件编译指令#ifdef, #ifndef, #else, #elif, #endif,宏的替换#define,添加行号,保留所有的编译器指令;
  • 编译:针对预处理后的文件进行词法分析、语法分析、语义分析、符号汇总、汇编代码生成,并针对程序的结构或者特定的 CPU 平台进行优化,其中涉及的过程较为复杂。简单来说编译的过程即为将 .cpp 源文件翻译成 .s 的汇编代码;
  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件,一个 .cpp 文件只会生成一个 .o 文件;
  • 链接:汇编程序生成的目标文件即为 .o 文件,单独的 .o 文件可能无法执行。因为一个程序可能由多个源文件组成,此时就存在多个 .o 文件。文件 A 中的函数引用了另一个文件 B 中定义的符号或者调用了某个库文件中的函数,这就需要 链接处理。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成一个可被操作系统加载执行的 ELF 程序文件。链接的过程较为复杂,如有兴趣可以参考:「Computer Systems: A Programmer’s Perspective」。

什么是链接

链接的种类

静态链接

动态链接

大端和小端

什么是字节序(Endian-ness)

两种字节序的形式

小端(Little-Endian)

大段(Bit-Endian)

字节序的转换

内存泄漏

什么是内存泄漏

程序块在[堆]中申请的[动态内存], 在程序块使用完成后没有得到[及时释放], 从而导致了堆中可使用的内存越来越少的现象

  • 内存泄漏是逻辑上的概念, 指在程序运行期间, 总内存的可复用率变低的问题
  • 内存泄漏主要指在[堆]上分配的变量所占用的内存, 因为[栈]中分配的变量, 会随着函数退出时自动回收. 而堆是动态分配的
  • 不仅要考虑 malloc, calloc, realloc, new 等分配的内存是否得到了释放, 也要考虑使用系统资源或者在[堆]中创建的对象

内存泄漏的检测与预防的实践

通常会利用各种[内存检测工具]来检测内存泄漏, 其原理也是统计和分析内存的申请和释放的相关日志

以下通过 valgrind 的实践来介绍

valgrind 实践

valgrind 是 Linux 下, 开源代码 (GPL V2) 的仿真调试工具的集合, 包括以下工具:

  • Memcheck:

c++ 内存管理

ELF 文件:

可执行与可链接格式(Executable and Linkable Format)是一种用于可执行文件, 目标代码, 共享库和核心转储(core dump)的标准文件格式, 每个ELF文件都由一个ELF header 和紧跟其后的文件数据部分组成, 可以参考 ELF 文件的构成如下:

alt text

我们可以看见可执行程序内部都是分段进行存储的.

  • .text section: 代码段, 通常存放已编译程序的机器代码, 一般操作系统加载后, 这部分是只读的
  • .rodata section: 只读数据段, 此段的数据不可修改, 存放程序中会使用的常量
  • data section: 数据段, 主要用于存放已初始化的全局变量, 常量
  • bss section: 未初始化的全局标量, 仅是占位符, 不占据任何实际磁盘空间

目标文件格式区分初始化和非初始化是为了空间效率

操作系统在加载 ELF 文件时, 会按照文件结构, 依次读取每个 section 中的内容, 并将其加载到内存中, 同时为该进程分配空间, 并将 pc寄存器 指向代码段的起始位置, 然后启动进程

内存分区

C++ 程序在运行时, 会按照不同的功能划分不同的段: 栈, 堆, 全局/静态存储区, 常量存储区, 代码区

  • 栈: 目前绝大多数CPU体系是基于栈来运行程序的, 栈中主要存放函数的局部变量, 函数参数, 返回地址等, 栈空间一般由操作系统进行默认分配或者程序指定分配, 栈空间在进程生存周期一直都存在, 当进程退出时, 操作系统才会对栈空间进行回收
  • 堆: 动态申请的内存空间, 就是由 malloc 函数或者 new 函数分配的内存块, 由系统控制它的分配和释放, 可以在程序运行周期随时进行申请和释放, 如果进程结束后还没收释放, 操作系统会自动回收
  • 全局区/静态存储区: 主要为 .bss 和 .data 段, 存放全局变量和静态变量, 程序运行结束, 操作系统自动释放, 在 C 中, 未初始化的放在 .bss 段中, 初始化的放在 .data 段中
  • 常量存储区: .rodata 段, 存放常量, 不允许修改, 程序运行结束后自动释放
  • 代码区: .text 段, 存放代码, 不允许修改, 但可以执行, 编译后的二进制文件存放在这里

参考 Linux 操作系统的内存分布如下:

从操作系统的本身而言, 以上存储区在该程序内存中的虚拟地址分布是如下形式(虚拟地址从低地址 到 高地址, 实际的物理地址可能是随机的): text->.data->.bss->heap->unused->stack->…

alt text

程序实例

以下为详细的程序实例, 也可以通过 GDB 工具来查看具体的每个变量的存放地址

#include <iostream>
using namespace std;

/*
* 说明: C++ 中不再区分初始化和未初始化的全局变量, 静态变量的存储区, 如果非要区分, 在下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char * gp_var; // gp_var 在全局区(.bss 段)

int main()
{
    int var;    // var 在 栈区
    char * p_var;   // p_var 在栈区
    char arr[] = "abc"; // arr 为数组变量, 存储在栈区: "abc" 为字符串常量, 存储在常量区
    char *p_var1 = "12345"; // p_var1 在栈区: "123456" 为字符串常量, 存储在常量区
    static int s_var = 0; // s_var 为静态变量, 存在静态存储区(.data 段)
    p_var = (char*)malloc(10) // 分配得来的10个字节的区域在堆区
    free(p_var);
    return 0;
}

变量定义与生命周期

作用域 与 生命周期

这两个属性, 以 空间(作用域) 和 时间(生命周期) 两个不同的维度来描述一个 变量

作用域

一个变量可以被引用的范围

  • 全局变量: 全局变量只需在一个源文件中定义, 就可以作用域所有的源文件, 其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量
  • 静态全局变量: 与全局变量的区别在于如果程序包含多个文件的话, 它作用域定义它的文件里, 不能作用到其他文件里, 即被 static 关键字修饰过的变量具有文件作用域, 即使两个不同的文件都定义了相同的名字的静态全局变量, 它们也是不同的变量
  • 局部变量: 它是自动对象(auto), 在程序运行期间不是一直存在,而是只在函数执行区间存在, 函数的依次调用执行结束后, 变量被撤销, 其所占的内存也被收回, 局部变量对于函数外部的程序说是不可见的, 实际是以 {} 为所用与
  • 静态局部变量: 它直被初始化依次, 自从第一次被初始化知道程序运行结束都一直存在, 它只对定义自己的函数体始终可见, 只有定义该变量的函数内部可以使用和访问该变量

生命周期

变量可以存在(被引用)的时间段

  • 全局变量: 在真个程序运行期间都会一直存在, 都可以随时访问, 当程序结束时, 对应的变量会自动销毁, 内存被系统回收
  • 局部变量: 仅限于函数被调用期间, 当函数调用结束时, 该变量会自动销毁
  • 静态局部变量: 在程序运行期间一直存在, 局部静态变量只能被初始化一次

从分配内存空间看

不同生命周期的变量, 在程序内存中的分布位置是不一样的, 内存分为 代码区(.text), 全局数据区(.data, .bss, .rodata), 堆区(heap), 栈区(stack), 不同的内存区域, 对应不同的生命周期

alt text

  • 静态变量一般存储在数据段, 包括 data 段. bss 段, rodata 段, 其中 data 存储已经初始化的静态变量和全局变量, bss 存储未初始化的静态变量与全局变量
  • 局部变量一般存储在 栈区 或者 堆区

Note

  • 静态变量和栈变量(存储在栈中的变量), 堆变量(存储在堆中的变量)的区别: 静态变量会被放在程序的静态存储区(.data 段, bss 段, rodata 段)中(静态变量会自动初始化, 这样可以在下一次调用的时候还可以保持原来的赋值), 而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值
  • 静态变量和全局变量的区别: 静态变量仅在变量的作用范围内可见, 实际是依靠编译器来操控作用域, 全局变量在整个程序范围内都可见, 只需要声明该全局变量, 即可使用
  • 全局变量的定义不要再头文件中定义: 如果在头文件中定义全局变量, 当该头文件被多个文件 #include 时, 该头文件中的全局变量就会被多次定义, 编译时会应为重复定义而报错, 一般情况下我们将变量的定义放在 .cpp 文件中, 在 .hpp 文件使用 extern 对变量进行声明

堆与栈

内存对齐

内存对齐

计算机中内存的地址空间是按照 byte 来划分的, 从理论上讲对任何类型变量的访问, 可以从内存中的任意地址开始, 但实际情况是: 在访问特定类型变量的时候通常在特定的内存地址访问, 这就需要对这些数据在内存中存放的位置进行限制, 各种类型数据按照一定的规划在空间上排列, 而不是顺序地一个接一个地排放, 这就是对齐, 编译器将程序中地每个 数据单元 的地址安排在机器字的整数倍的地址指向的内存之中

为什么要内存对齐

主要是由于 CPU 的访问内存的特性决定, CPu 访问内存时并不是以字节为单位来读取内存, 而是以机器字长为单位, 实际机器字长由 CPU 数据总线宽度决定的, 实际 CPU 运行时, 每一次控制内存读写信号发生时, CPU 可以从内存中读取数据总线宽度的数据, 并将其写入到 CPU 的通用寄存器中

比如, 32位cpu, 机器字长为 4字节, 数据总线宽度为 32 为, 如果该 CPU 的地址总线宽度也为 32 位, 则其可以访问的地址空间为[0, 0xffffffff]

内存对齐的主要目的是为了减少 CPU 访问内存的次数, 加大 CPU 访问内存的吞吐量

假设: 读取 8 个字节的数据, 按照每次读取 4 个字节的速度, 则 8 个字节需要 CPU 耗费 2 次读取操作

CPU 始终以字节访问内存, 如果不进行内存对齐, 很可能增加 CPU 访问内存的次数

alt text

比如: 以上在读取变量 b 时, 如果不进行内存对齐的话, 会导致 CPU 读取次数为 2, 在内存对齐的情况下, 只需读取一次即可, 当然实际的读取非对齐的内存处理更为复杂, 我们参考下图中读取非对齐内存时的数据处理过程

alt text

除了能够减少内存访问次数, 增加内存读取的吞吐量以外, 还有其他原因

  • 比如某些特性的硬件设备只能存取对齐数据, 存取非对齐的数据可能会引发一场, 比如 CPU 中 SIMD 指令, 则必须要求内存严格对齐
  • 每次内存访问是原子的, 如果变量的大小不超过字长, 那么内存对齐后, 对该变量的访问就是原子的. 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作, 因此此时 CPU 可能需要读取多次内存, 这样就破坏了 变量的原子性
  • 相比于存取对齐的数据, 存取非对齐的数据需要花费更多的时间, 提高内存的访问效率, 因为 CPU 在读取内存时, 是一块一块地读取
  • 某些处理器虽然支持非对齐数据的访问, 但会引发 对齐陷阱(alignment trap)
  • 某些硬件设备只支持简单数据指令而非对齐存取, 不支持复杂数据指令的非对齐存取

内存对齐的原则

程序中的内存对齐大部分是由编译器来处理, 编译器会自动在内存之间填充字节, 结构体中的变量对齐的基本规则如下:

  • 结构体变量的首地址能够被其最宽的基本类型成员的长度和对齐基数二者中的较小者整除;
  • 结构体中的 static 成员变量不占用结构体的空间, 由于静态成员变量在程序初始化时已经在静态存储区分配完成, 所有该结构体实例中的静态成员都指向同一个内存区域;
  • 结构体中每个成员相对于结构体首地址的偏移量(offset)都是该成员大小与对齐基数中的较小者的整数倍, 如有需要, 编译器会在成员之间加上 填充字节(internal padding)
  • 结构体的总大小为结构体中最宽基本类型成员的长度与对齐基数二者中的较小者的整数倍, 如有需要编译器会在最末尾的成员之后加上 填充字节(internal padding)

指定程序对齐规则

我们可以指定结构体的对齐规则, 在某些特定场景下, 我们需要指定结构体内存进行对齐

例如: 在发送特定网络协议报文, 硬件协议控制, 消息传递, 硬件寄存器访问时, 这时, 就需要避免内存对齐, 因为双方均按照预定义的消息格式来进行交互, 从而避免不同的硬件平台造成的差异, 同时能够将双方传递的数据进行空间压缩, 避免不必要的空间浪费

programpack: 通过使用 #progma pack(x) 指定结构体以 x 为单位进行对齐

#pragma pack (push)
#pragma pack (x)
// 存放需要 x 对齐方式的数据块
#pragma pack (pop)

attribute((aligned(n))): __attribute__((aligned (n))) 让所作用的结构成员对齐在 n 字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。需要注意的是:内存对齐的对齐数取决于对齐系数和成员的字节数两者之中的较小值。对齐属性的有效性会受到链接器(linker)固有限制的限制,即如果你的链接器仅仅支持 8 字节对齐,即使你指定16字节对齐,那么它也仅仅提供 8 字节对齐。__attribute__((packed)) 表示取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,这部分属于 gcc 中的用法,详细了解详情可以参考 gcc 的手册

alignof: C++ 11 以后新增 alignof 的特性,通过调用 alignof 返回当前变量的字节对齐方式

小结

内存对齐使得程序便于在不同的平台之间进行移植, 因为有些硬件平台不能够支持任意地址的数据访问, 只能在某些地址处取某些特定的数据, 否则会抛出异常; 另一方面提高内存的访问效率, 因为 CPU 在读取内存时, 是以块为单位进行读取的

智能指针的创建

make_unique 和 make_share

优先使用 std:make_unique 和 std:make_shared, 而非使用 new;

  • make_unique: 减少代码量, 能够加快编译速度, 定义两边类型时, 编译器需要进行类型推导, 会降低编译速度, 某些意外情况下可能会导致内存泄漏, 但是 make_unique 不允许自定析构器, 不接受 std:initializer_list 对象
  • make_shared: 这个主要是可以减少对堆中申请内存的次数, 只需要申请一次即可

智能指针简介与使用

智能指针

为了解决动态内存分配时带来的内存泄露以及多次释放同一块内存空间而提出的

传统的指针在申请完成后, 必须要调用 free 或 delete 来释放指针, 否则容易产生内存泄漏的问题

smart pointer 遵循 RAII 原则, 当 smart pointer 对象创建时, 即为该指针分配了相应的内存, 当对象销毁时, 析构函数会自动释放内存, 需要注意的是, 智能指针不能像普通指针那样支持加减运算(迭代操作)

智能指针的类型

  • unique_ptr: 独享所有权的智能指针, 资源智能被一个指针占有, 该指针不能拷贝构造和赋值, 但可以进行移动构造和移动赋值构造(调用 move() 函数), 即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象, 可以通过该方法进行赋值

alt text

  • shared_ptr: 与 unique_ptr 不同的是, shared_ptr 中的资源可以被多个指针共享, 但是多个指针指向同一个资源不能被释放多次, 因此使用计数机制表明资源被几个指针共享; 通过 use_count() 查看资源的所有者的个数, 可以通过 unique_ptr, weak_ptr 来构造, release 释放资源的所有权, 同时将计数减一, 当计数减为 0 时候,会自动释放内存空间, 从而避免了泄露 需要注意的是, shared_ptr 并不是线程安全的, 但 shared_ptr 的计数是原子操作实现的, 利用 atmoic CAS 指令实现 通过 shared_ptr 的内存模型, 当引用计数和 weak cout 同时为 0 时, shared_ptr 对象才会被最终释放掉 通过查看 shared_ptr 的源代码可以看到如下, shared_ptr 实际成员包含两个指针, 一个指向对象资源的指针 ptr, 另一个指向管理区域的指针 ___cntrl_, 具体 __cntrl_ 指针的区域包括 deleter, allocator, shared_ptr 对象的引用计数, weak_ptrs 的对象的引用计数

  • weak_ptr: 指向 shared_ptr 指向的对象, 能够解决 shared_ptr 带来的循环引用的问题, 与 shard_ptr 配合使用, 将 weak_ptr 转换为 shared_ptr 时, 虽然它能访问 shared_ptr 所指向的资源, 但却不享有资源的所有权, 不影响该资源的引用计数, 有可能资源已经被释放, 但 weak_ptr 仍然存在, shared_ptr 必须等待所有引用 weak_ptr 全部被释放才会进行释放, 因此, 每次访问资源时, 都需要判断资源是否有效.

什么是类

c++ 在visual studio和XCode上的差异

描述

Visual Studio 和 Xcode 在处理 C++ 方面的主要差异在于它们所使用的编译器、工具链以及它们各自的集成开发环境(IDE)特性。以下是一些关键的不同点:

1. 编译器

  • Visual Studio 使用的是 Microsoft 的 C++ 编译器(MSVC),该编译器有自己的语法扩展和特性,虽然它也支持大部分的 C++ 标准。
  • Xcode 使用的是 LLVM/Clang 编译器,这是一个开源编译器,其设计目标之一就是提供清晰的诊断信息,并且它对现代 C++ 标准的支持非常好。

2. IDE 特性

  • Visual Studio 提供了丰富的功能,如智能感知(IntelliSense)、实时模板编辑、重构工具等,这些都是针对微软的开发环境优化的。
  • Xcode 也有类似的功能,如自动补全(Code Completion)、实时错误提示(Live Issue Identification)等,这些功能针对 macOS 和 iOS 开发进行了优化。

3. API 和库支持

  • 在 Visual Studio 中,可以使用 Windows API 来开发应用程序,以及 Microsoft 提供的其他库。
  • 在 Xcode 中,更可能使用 Cocoa 或 Cocoa Touch(对于 iOS 开发)API,以及 BSD 和 POSIX 的相关库。

4. 构建系统

  • Visual Studio 使用 MSBuild 构建系统来管理项目。
  • Xcode 使用自己的构建系统,不过也可以配置使用外部构建工具如 CMake。

5. 调试工具

  • Visual Studio 自带强大的调试工具,如 WinDbg。
  • Xcode 使用 LLDB 作为调试器,LLDB 提供了现代化的命令行接口和 GUI 调试体验。

6. 用户界面

  • Visual Studio 的用户界面设计适合 Windows 用户,具有熟悉的 Windows 风格界面。
  • Xcode 的用户界面则更适合 macOS 用户,遵循苹果的设计指南。

7. 跨平台支持

  • 如果在 Visual Studio 中开发跨平台的 C++ 代码,可能会使用像 CMake 这样的工具来帮助管理跨平台的构建过程。
  • 在 Xcode 中,虽然主要面向 macOS 和 iOS 开发,但也可以通过配置支持跨平台的开发,尤其是在使用像 CMake 这样的工具时。

总结

尽管两者在很多方面有所不同,但在编写符合 C++ 标准委员会定义的标准 C++ 代码时,这些差异并不显著。如果目标是编写可移植的 C++ 代码,应该尽量避免使用特定于编译器的特性,并遵循 C++ 标准。此外,使用像 CMake 这样的跨平台构建工具可以在多个平台上保持一致性。

如何避免写 c 代码的时候 Unhandled Exception:Stack overflow

避免 C 程序出现堆栈溢出异常(Stack Overflow Exception)通常需要注意以下几点

  1. 优化递归算法:如果程序中使用了递归,确保递归深度可控,避免无限递归或递归层次过深。
  2. 减少局部变量的数量和大小:函数内部声明的局部变量会占用堆栈空间,特别是对于大型数组或结构体。优化代码,尽量减少或优化局部变量的使用。
  3. 动态内存分配:考虑使用动态内存分配函数(如 malloc()、calloc()、realloc())来管理大型数据结构,而不是将它们放在栈上。
  4. 避免过度递归:如果递归是必须的,确保递归调用的深度受到控制,并考虑是否可以改写为迭代方法。
  5. 增加堆栈空间限制:虽然不推荐频繁增加堆栈空间的大小,但是可以通过编译器或操作系统的设置,适当增加堆栈空间的限制。这种方法应谨慎使用,并仅当确实需要时才考虑。
  6. 使用静态分配:对于较小的数据结构或变量,优先使用静态分配(即在函数外部或静态存储区声明变量)而不是动态分配或放在堆栈上。
  7. 编译器优化:现代编译器通常会进行优化,包括函数调用和参数传递的优化。确保使用编译器的优化选项,以提高程序的效率和性能。
  8. 定期检查和测试:对于可能存在堆栈溢出风险的代码,定期进行检查和测试是很重要的,以发现潜在的问题并进行优化和改进。

左值和右值

stl

描述

STL(Standard Template Library,标准模板库)是 C++ 标准库的重要组成部分,提供了 高效且通用的数据结构和算法。

STL 的核心要素主要包括以下几个方面

  • 容器(Containers): 容器是用于存储和组织数据的数据结构。STL 提供了多种类型的 容器,每种容器都有其特定的特性和使用场景。
  • 迭代器(Iterators): 迭代器是用于遍历容器中元素的指针或指针类似的对 象。STL 提供了多种类型的迭代器,以适应不同容器的需求。
  • 算法(Algorithms): 算法是用于处理容器中数据的函数模板。STL 提供了大量的算法 ,涵盖了排序、查找、修改、数值计算等多种操作。
  • 函数对象(Function Objects): 函数对象是重载了函数调用运算符 operator() 的对 象,可以像普通函数一样调用。STL 提供了一些预定义的函数对象,用于算法中的比较和 操作
  • 适配器(Adapters): 适配器用于改变现有容器的行为,使其符合特定的需求。

迭代器相关

描述

容器相关

描述

stl 提供的数据类型

序列容器

序列容器存储元素的顺序集合,支持随机访问或顺序访问。

  • vector: array

    • 特点: 动态数组, 支持随机访问, 末尾插入和删除效率高
    • 使用场景:适用于需要频繁随机访问和在末尾进行插入和删除操作的场景。
      #include <vector>
      #include <iostream>
    
      int main() {
          std::vector<int> vec = {1, 2, 3, 4, 5};
          vec.push_back(6); // 在末尾添加元素
          std::cout << "Vector size: " << vec.size() << std::endl; // 输出: Vector size: 6
          return 0;
      }
    
  • list:

    • 特点: 双向链表,支持高效的插入和删除操作,但不支持随机访问。

    • 使用场景:适用于需要频繁在任意位置进行插入和删除操作的场景

      #include <list>
      #include <iostream>
      
      int main() {
          std::list<int> lst = {1, 2, 3, 4, 5};
          lst.push_front(0); // 在头部添加元素
          lst.push_back(6);  // 在末尾添加元素
          for (int val : lst) {
              std::cout << val << " "; // 输出: 0 1 2 3 4 5 6
          }
          return 0;
      }
      
  • deque:

    • 特点:双端队列,支持高效的前端和后端插入和删除操作,也支持随机访问

    • 使用场景:适用于需要在两端进行插入和删除操作的场景。

      #include <deque>
      #include <iostream>
      
      int main() {
          std::deque<int> deq = {1, 2, 3, 4, 5};
          deq.push_front(0); // 在头部添加元素
          deq.push_back(6);  // 在末尾添加元素
          for (int val : deq) {
              std::cout << val << " "; // 输出: 0 1 2 3 4 5 6
          }
          return 0;
      }
      

关联容器

关联容器存储键值对,支持高效的查找操作

  • set:

    • 特点:有序集合,不允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值且有序存储的场景。

      #include <set>
      #include <iostream>
      
      int main() {
          std::set<int> s = {5, 3, 1, 4, 2};
          s.insert(6); // 插入元素
          for (int val : s) {
              std::cout << val << " "; // 输出: 1 2 3 4 5 6
          }
          return 0;
      }
      
  • map:

    • 特点:有序键值对集合,不允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且有序存储键值对的场景。
      #include <map>
      #include <iostream>
    
      int main() {
          std::map<std::string, int> m = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};
          m["date"] = 4; // 插入键值对
          for (const auto& pair : m) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    
  • multiset: -特点:有序集合,允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值但允许重复且有序存储的场景。
      #include <set>
      #include <iostream>
    
      int main() {
          std::multiset<int> ms = {5, 3, 1, 4, 2, 3};
          ms.insert(6); // 插入元素
          for (int val : ms) {
              std::cout << val << " "; // 输出: 1 2 3 3 4 5 6
          }
          return 0;
      }
    
  • multimap:

    • 特点:有序键值对集合,允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值但允许重复且有序存储键值对的场景
      #include <map>
      #include <iostream>
    
      int main() {
          std::multimap<std::string, int> mm = {{"apple", 1}, {"banana", 2}, {"apple", 3}};
          mm.insert({"cherry", 4}); // 插入键值对
          for (const auto& pair : mm) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    

无序容器

无序容器存储键值对,支持高效的查找操作,但不保证元素的顺序。

  • unordered_set:

    • 特点:无序集合,不允许重复元素,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且不需要有序存储的场景。
      #include <unordered_set>
      #include <iostream>
    
      int main() {
          std::unordered_set<int> us = {5, 3, 1, 4, 2};
          us.insert(6); // 插入元素
          for (int val : us) {
              std::cout << val << " "; // 输出顺序不确定
          }
          return 0;
      }
    
  • unordered_map

    • 特点:无序键值对集合,不允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值且不需要有序存储键值对的场景。
      #include <unordered_map>
      #include <iostream>
    
      int main() {
          std::unordered_map<std::string, int> um = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};
          um["date"] = 4; // 插入键值对
          for (const auto& pair : um) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    
  • unordered_multiset

    • 特点:无序集合,允许重复元素,支持高效的查找、插入和删除操作。

    • 使用场景:适用于需要唯一键值但允许重复且不需要有序存储的场景

      #include <unordered_set>
      #include <iostream>
      
      int main() {
          std::unordered_multiset<int> ums = {5, 3, 1, 4, 2, 3};
          ums.insert(6); // 插入元素
          for (int val : ums) {
              std::cout << val << " "; // 输出顺序不确定
          }
          return 0;
      }
      
  • unordered_multimap

    • 特点:无序键值对集合,允许重复键,支持高效的查找、插入和删除操作。
    • 使用场景:适用于需要唯一键值但允许重复且不需要有序存储键值对的场景。
      #include <unordered_map>
      #include <iostream>
    
      int main() {
          std::unordered_multimap<std::string, int> umm = {{"apple", 1}, {"banana", 2}, {"apple", 3}};
          umm.insert({"cherry", 4}); // 插入键值对
          for (const auto& pair : umm) {
              std::cout << pair.first << ": " << pair.second << std::endl;
          }
          return 0;
      }
    

容器适配器

容器适配器基于其他容器构建,提供特定的功能。

  • stack

    • 特点:后进先出(LIFO)的数据结构,基于 deque、vector 或 list 实现。
    • 使用场景:适用于需要栈操作的场景。
      #include <stack>
      #include <iostream>
    
      int main() {
          std::stack<int> stk;
          stk.push(1);
          stk.push(2);
          stk.push(3);
          std::cout << "Top element: " << stk.top() << std::endl; // 输出: Top element: 3
          stk.pop();
          std::cout << "Top element after pop: " << stk.top() << std::endl; // 输出: Top element after pop: 2
          return 0;
      }
    
  • queue

    • 特点:先进先出(FIFO)的数据结构,基于 deque 或 list 实现。
    • 使用场景:适用于需要队列操作的场景。
      #include <queue>
      #include <iostream>
    
      int main() {
          std::queue<int> q;
          q.push(1);
          q.push(2);
          q.push(3);
          std::cout << "Front element: " << q.front() << std::endl; // 输出: Front element: 1
          q.pop();
          std::cout << "Front element after pop: " << q.front() << std::endl; // 输出: Front element after pop: 2
          return 0;
      }
    
  • priority_queue

    • 特点:最大堆(默认)或最小堆的数据结构,基于 vector 实现。
    • 使用场景:适用于需要优先级队列操作的场景。
      #include <queue>
      #include <iostream>
    
      int main() {
          std::priority_queue<int> pq;
          pq.push(3);
          pq.push(1);
          pq.push(2);
          std::cout << "Top element: " << pq.top() << std::endl; // 输出: Top element: 3
          pq.pop();
          std::cout << "Top element after pop: " << pq.top() << std::endl; // 输出: Top element after pop: 2
          return 0;
      }
    

stl 算法分析

描述

函数对象

描述

  • 比较函数对象: less, greater, equal_to, not_equal_to 等。
  • 算术函数对象: plus, minus, multiplies, divides, modulus, negate 等。
  • 逻辑函数对象: logical_and, logical_or, logical_not 等

taipy

Taipy

Taipy 是一个用于构建数据驱动应用程序的开源框架。它旨在简化和加速开发数据应用程序的过程,提供了一组工具和库,帮助开发者快速创建可视化、数据分析和机器学习模型的应用程序。Taipy 的目标是使数据科学家和开发者能够更高效地将他们的分析和模型转换为可交付的应用程序。

Taipy

Taipy

一个用于构建数据驱动应用程序的开源框架。它旨在简化和加速开发数据应用程序的过程,提供了一组工具和库,帮助开发者快速创建可视化、数据分析和机器学习模型的应用程序。Taipy 的目标是使数据科学家和开发者能够更高效地将他们的分析和模型转换为可交付的应用程序。

Taipy 可以做什么

Taipy 安装

Taipy 入门

design

消息队列, 生产与消费

描述

问题的产生, 在于一个计算逻辑中的两个柱体 A 和 B 之间存在数据的生产先后顺序, 需要用 A 生成 B 的一个前置数据, 再由 B 输出产生结果, 而 A 中的数据在时间上没有关联性, 即 t 时刻生成的 数据 \(A_t\) 与 t+1 时刻 \(A_{t+1}\) 无关

为了程序主逻辑运行的效率, 对 A 和 B 采用异步执行, 即当 在 A 生成数据后, B再开始运行

mathmatic

半平面

cmake

在 visual studio 上如何使用 CMake 生成项目

准备工作

确保系统已经安装了 Visual Studio 并且包含了 C++ 开发工具。此外,Visual Studio 自 带了 CMake 支持,所以你不需要单独安装 CMake

创建或获取 CMake 项目

  • 已有项目:如果已有一个使用 CMake 的项目,可以直接跳到“导入项目”部分。
  • 新项目:如果要从头开始创建一个项目,先创建一个包含 CMakeLists.txt 文件的目录结 构。CMakeLists.txt 是 CMake 的配置文件,其中定义了如何构建你的项目。

导入项目

  • 打开 Visual Studio。
  • 选择 “File” > “Open” > “CMake…” 菜单项。
  • 浏览并选择你的 CMakeLists.txt 文件所在的目录,然后点击 “打开” 按钮。

配置 CMake

  • 在解决方案资源管理器中,会看到项目已经被列出来了。Visual Studio 会自动检测W CMakeLists.txt 文件并尝试解析它。
  • 如果需要指定特定的 CMake 参数或选项,可以在 Visual Studio 的 “CMake 设置” 对话 框中进行设置。右击解决方案资源管理器中的项目名,然后选择 “CMake Settings” 进行 编辑。
  • 在这里你可以更改生成器、配置类型(如 Debug 或 Release)、以及添加额外的 CMake 变量等。

构建项目

  • 一旦项目被正确识别和配置,就可以直接从 Visual Studio 内部构建项目了。点击顶 部菜单栏的 “Build” > “Build All” 来编译整个项目。
  • 也可以右击解决方案资源管理器中的具体目标(Target),然后选择 “Build” 来只构建 该项目的目标

注意事项

  • 确保 CMake 版本与 Visual Studio 兼容。
  • 如果遇到任何问题,检查输出窗口中的错误信息,这可以帮助你快速定位问题所在。
  • 使用 CMake 的高级功能可能需要更深入地了解 CMake 语法和概念。

csharp

Task 的使用技巧

描述

使用 Task 时,有几个技巧可以帮助你更高效地进行并行编程:

  • 使用 Task.Run 来简化任务创建:对于简单的后台工作,Task.Run 是最便捷的方式,它自动使用线程池,并简化了任务创建。

  • 利用 async 和 await:结合 async 和 await 关键字来处理异步操作,使代码更易于阅读和维护,同时避免了回调地狱。

  • 处理异常:使用 try-catch 块来捕获 await 异步操作中的异常,或者在 Task.ContinueWith 中处理任务的异常,以避免未处理的异常导致程序崩溃。

  • 任务取消:使用 CancellationToken 来支持任务的取消操作。创建任务时,传递一个 CancellationToken,并在任务内部检查该 token,以便在需要时中断任务。

  • 任务组合:使用 Task.WhenAll 和 Task.WhenAny 来等待多个任务的完成或处理第一个完成的任务。这对并发操作非常有用。

  • 避免长时间运行的任务:尽量将长时间运行的任务拆分为多个较短的任务,以避免阻塞线程池中的线程,影响其他任务的执行。

  • 使用 Task.Run 和 ConfigureAwait(false):在不需要恢复到原始上下文的情况下,使用 ConfigureAwait(false) 来提高性能,特别是在库或服务中,避免不必要的上下文切换。

通过这些技巧,你可以更高效地使用 Task 来编写异步、并行的代码,并提升程序的性能和响应性

Task 和 Thread 的关系

描述

Task 和 Thread 都用于并行编程,但它们有不同的特性和用途:

  • 抽象层次:Thread 是底层的线程管理类,直接操作线程。Task 是更高级的抽象,提供了对线程的封装,并支持异步编程模型。

  • 资源管理:Task 通常使用线程池来管理和复用线程资源,减少了线程创建和销毁的开销。Thread 是直接创建和管理的,每个线程都需要独立的资源,可能会导致性能问题。

  • 编程模型:Task 提供了更简洁的编程模型,支持任务链、异常处理、取消等功能。Thread 需要手动管理这些方面,代码较为繁琐。

  • 调度:Task 由 TaskScheduler 调度,支持任务优先级和调度策略。Thread 由操作系统调度,缺乏灵活性。

总的来说,Task 提供了更高层次的并行编程支持,更适合大多数现代应用,而 Thread 提供了对线程的直接控制,适合需要精细化线程管理的场景。

Task 的一些使用技巧对 Thread 的封装

Task.Run() 和 Task.Factory.StartNew() 的区别

描述

Task.Run 和 Task.Factory.StartNew 都用于创建和启动任务,但它们有一些区别:

  • 默认行为:Task.Run 是 Task.Factory.StartNew 的一个简化版本,通常使用默认的 TaskCreationOptions 和 TaskScheduler。Task.Factory.StartNew 提供了更多的配置选项。

  • 配置选项:Task.Factory.StartNew 允许你设置更多的参数,比如 TaskCreationOptions 和 TaskScheduler。而 Task.Run 是为了简化任务创建,默认使用 TaskCreationOptions.DenyChildAttach 和当前线程的调度器。

  • 延续性:Task.Run 的延续性(ContinueWith)可以更简单地处理,因为它默认使用线程池来执行任务,适用于大多数常见的并行处理场景。

综上所述,如果你需要更多控制和配置,可以使用 Task.Factory.StartNew;而如果只是需要简单地创建和启动一个任务,Task.Run 是更方便的选择

TaskCreationOptions

TaskScheduler

从 Task 到 Thread

描述

cadobjectarx

Polyline 的对象信息里的 `Bulge`

Bulge

Bulge 是在多段线(Polyline)对象的弧段(Arc Segment)中存在的一个属性。其是与 多段线的各个顶点(vertices)相关的,用于定义从一个顶点到下一个顶点之间的弧段的弯 曲程度。Bulge 的计算公式为

$$ Bulge = \tan{\frac{\theta}{4}}$$

其中 \(\theta\) 是圆心角

Bulge 的性质

  • 正值和负值:Bulge 的正负值决定了弧的方向。如果 Bulge 是正值,弧段逆时针方 向弯曲;如果是负值,弧段顺时针方向弯曲。
  • 值的大小:Bulge 的绝对值越大,弧段的弯曲程度越大;绝对值越小,弧段的弯曲程 度越小。

在 多段线中 (Polyline)

  • 多段线可以包含直线段和弧段。每个顶点之间的段可以是直线或弧线。

    在 AutoCAD 中,多段线(Polyline)确实只能由直线段和弧线段组成

  • 在多段线中,每个顶点都有一个与之关联的 bulge 值。这个 bulge 值用于定义从当前顶 点到下一个顶点之间的段是直线还是弧线。

对于弧线 (Arc)

  • 弧对象是一个独立的几何对象,表示一个圆弧
  • 弧对象本身没有 bulge 属性,而是通过起点、终点、圆心和半径来定义

CAD 中的曲线 及其 离散化的操作

在 AutoCAD 中,对曲线的定义及其类;

AutoDesk Document

Autodesk.AutoCAD.DatabaseServices.DBObject
    Autodesk.AutoCAD.DatabaseServices.Entity
        Autodesk.AutoCAD.DatabaseServices.Curve
            Autodesk.AutoCAD.DatabaseServices.Arc
            Autodesk.AutoCAD.DatabaseServices.Circle
            Autodesk.AutoCAD.DatabaseServices.Ellipse
            Autodesk.AutoCAD.DatabaseServices.Leader
            Autodesk.AutoCAD.DatabaseServices.Line
            Autodesk.AutoCAD.DatabaseServices.Polyline
            Autodesk.AutoCAD.DatabaseServices.Polyline2d
            Autodesk.AutoCAD.DatabaseServices.Polyline3d
            Autodesk.AutoCAD.DatabaseServices.Ray
            Autodesk.AutoCAD.DatabaseServices.Spline
            Autodesk.AutoCAD.DatabaseServices.Xline

圆弧(Arc)

圆(Circle)

椭圆(Ellipse)

引线(Leader)

直线(Line)

多段线(Polyline)

射线(Ray)

样条曲线(Spline)

无限线(XLine)