C++学习:六个月从基础到就业——内存管理:new/delete操作符

C++学习:六个月从基础到就业——内存管理:new/delete操作符

本文是我C++学习之旅系列的第十七篇技术文章,也是第二阶段"C++进阶特性"的第二篇,主要介绍C++中动态内存管理的核心操作符——new和delete。查看完整系列目录了解更多内容。

引言

在上一篇文章中,我们深入探讨了堆和栈的概念以及它们在内存管理中的作用。本文将聚焦于C++中用于动态内存分配和释放的基本工具——newdelete操作符。与许多高级语言不同,C++允许程序员直接控制内存的分配和释放,这提供了极大的灵活性,但同时也带来了更多的责任和潜在的陷阱。

动态内存管理是现代软件开发中不可或缺的部分,尤其是当处理大型数据结构、运行时大小未知的数据或需要在程序执行过程中持久存在的对象时。理解newdelete的工作原理及正确使用方法,对于编写高效、稳定的C++程序至关重要。

new和delete基础

new操作符概述

new操作符用于动态分配内存,它执行三个主要步骤:

  1. 分配足够大的未初始化内存来存储指定类型的对象
  2. 调用构造函数初始化对象(如果是类类型)
  3. 返回指向新创建对象的指针

基本语法:

Type* ptr = new Type;           // 分配单个对象
Type* arr = new Type[size];     // 分配对象数组

delete操作符概述

delete操作符用于释放动态分配的内存,它执行两个主要步骤:

  1. 调用对象的析构函数(如果是类类型)
  2. 释放内存

基本语法:

delete ptr;      // 释放单个对象的内存
delete[] arr;    // 释放数组的内存

简单示例

下面是一个使用newdelete的基本示例:

#include <iostream>class Simple {
public:Simple() {std::cout << "Simple constructor called" << std::endl;}~Simple() {std::cout << "Simple destructor called" << std::endl;}void sayHello() {std::cout << "Hello from Simple object!" << std::endl;}
};int main() {// 分配单个对象Simple* obj = new Simple;obj->sayHello();delete obj;  // 释放单个对象std::cout << "-------------------" << std::endl;// 分配对象数组Simple* objArray = new Simple[3];objArray[0].sayHello();objArray[1].sayHello();objArray[2].sayHello();delete[] objArray;  // 释放数组return 0;
}

输出结果:

Simple constructor called
Hello from Simple object!
Simple destructor called
-------------------
Simple constructor called
Simple constructor called
Simple constructor called
Hello from Simple object!
Hello from Simple object!
Hello from Simple object!
Simple destructor called
Simple destructor called
Simple destructor called

new操作符详解

分配过程详解

当我们使用new操作符时,它实际执行以下操作:

  1. 调用底层的内存分配函数(通常是operator new)分配足够的原始内存
  2. 将原始内存转换为适当的类型指针
  3. 使用适当的构造函数初始化对象(对于非POD类型)

new的变体

带初始化的new
int* p1 = new int;        // 未初始化值
int* p2 = new int();      // 初始化为0
int* p3 = new int(42);    // 初始化为42// C++11后,可以使用统一初始化语法
int* p4 = new int{42};    // 初始化为42
带位置的new (placement new)

Placement new允许在预先分配的内存位置构造对象,而不分配新内存:

#include <iostream>
#include <new>  // 为placement new包含此头文件class Complex {
private:double real;double imag;public:Complex(double r, double i) : real(r), imag(i) {std::cout << "Constructor called." << std::endl;}~Complex() {std::cout << "Destructor called." << std::endl;}void print() const {std::cout << real << " + " << imag << "i" << std::endl;}
};int main() {// 分配一块足够大的内存,但不构造对象char memory[sizeof(Complex)];// 在预先分配的内存上构造对象Complex* obj = new(memory) Complex(3.0, 4.0);obj->print();// 显式调用析构函数(不要使用delete,因为内存不是通过new分配的)obj->~Complex();return 0;
}

Placement new主要用于:

  • 优化内存分配(避免多次分配/释放)
  • 内存池实现
  • 对象的精确放置(如硬件通信缓冲区)
nothrow new

默认情况下,当new无法分配内存时会抛出std::bad_alloc异常。但我们也可以使用nothrow形式,失败时返回nullptr而不是抛出异常:

#include <iostream>
#include <new>int main() {// 尝试分配巨大的内存(可能失败)int* hugeArray = new(std::nothrow) int[1000000000000];if (hugeArray == nullptr) {std::cout << "Memory allocation failed." << std::endl;} else {std::cout << "Memory allocation succeeded." << std::endl;delete[] hugeArray;}return 0;
}

分配数组

分配数组时,new会记录数组的大小,以便delete[]能正确释放全部内存:

int* array = new int[10];  // 分配10个整数的数组// 使用数组...delete[] array;  // 释放整个数组

注意:当分配数组时必须使用delete[]而非delete来释放内存,否则可能导致未定义行为。

二维数组的分配

有几种方式可以分配二维数组:

// 方法1:使用连续内存(推荐)
int rows = 3, cols = 4;
int* matrix = new int[rows * cols];
matrix[row * cols + col] = value;  // 访问元素// 方法2:数组的数组(指针数组)
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {matrix[i] = new int[cols];
}// 使用...// 释放方法2的内存
for (int i = 0; i < rows; i++) {delete[] matrix[i];
}
delete[] matrix;
使用std::vector替代动态数组

通常,使用std::vector比原始动态数组更安全:

#include <vector>// 一维vector
std::vector<int> vec(10);  // 大小为10的向量// 二维vector
std::vector<std::vector<int>> matrix(rows, std::vector<int>(cols));
matrix[row][col] = value;  // 访问元素

std::vector会自动管理内存分配和释放,减少了内存泄漏和错误的风险。

delete操作符详解

释放过程详解

当我们使用delete操作符时,它执行以下操作:

  1. 如果指针非空,调用对象的析构函数(对于非POD类型)
  2. 调用底层的内存释放函数(通常是operator delete)来释放内存

delete与析构函数

delete的关键功能之一是调用对象的析构函数。对于类对象,析构函数负责清理资源(如关闭文件、释放其他动态分配的内存等)。

class ResourceManager {
private:int* data;public:ResourceManager() {data = new int[1000];std::cout << "Resource acquired" << std::endl;}~ResourceManager() {delete[] data;std::cout << "Resource released" << std::endl;}
};int main() {ResourceManager* rm = new ResourceManager();delete rm;  // 调用析构函数,确保data被正确释放return 0;
}

如果不调用delete,析构函数不会被执行,导致data指向的内存泄漏。

delete[]与数组

delete[]操作符用于释放通过new[]分配的数组。它确保数组中的每个对象都正确调用其析构函数。

class MyClass {
public:MyClass() {std::cout << "Constructor called" << std::endl;}~MyClass() {std::cout << "Destructor called" << std::endl;}
};int main() {MyClass* array = new MyClass[3];// 输出: Constructor called (3次)delete[] array;  // 对数组中的每个对象调用析构函数// 输出: Destructor called (3次)return 0;
}

使用错误的删除形式可能导致严重问题:

  • 使用delete释放通过new[]分配的内存:可能只调用第一个对象的析构函数,导致其他对象的资源泄漏
  • 使用delete[]释放通过new分配的单个对象:未定义行为,可能导致崩溃

new和delete的内部机制

operator new与operator delete函数

newdelete操作符实际上调用了底层的函数operator newoperator delete来执行内存分配和释放:

void* operator new(std::size_t size);
void operator delete(void* ptr) noexcept;void* operator new[](std::size_t size);
void operator delete[](void* ptr) noexcept;

这些函数可以被重载,以实现自定义内存管理策略:

#include <iostream>
#include <cstdlib>// 重载全局operator new
void* operator new(std::size_t size) {std::cout << "Custom global operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;
}// 重载全局operator delete
void operator delete(void* ptr) noexcept {std::cout << "Custom global operator delete" << std::endl;std::free(ptr);
}class MyClass {
public:MyClass() {std::cout << "MyClass constructor" << std::endl;}~MyClass() {std::cout << "MyClass destructor" << std::endl;}// 类特定的operator new重载void* operator new(std::size_t size) {std::cout << "MyClass::operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 类特定的operator delete重载void operator delete(void* ptr) noexcept {std::cout << "MyClass::operator delete" << std::endl;std::free(ptr);}
};int main() {// 使用全局operator newint* pi = new int;delete pi;std::cout << "-------------------" << std::endl;// 使用类特定的operator newMyClass* obj = new MyClass;delete obj;return 0;
}

输出:

Custom global operator new: 4 bytes
Custom global operator delete
-------------------
MyClass::operator new: 1 bytes
MyClass constructor
MyClass destructor
MyClass::operator delete

内存对齐与内存布局

new操作符确保分配的内存满足对象的对齐要求。不同类型有不同的对齐要求:

std::cout << "alignof(char): " << alignof(char) << std::endl;
std::cout << "alignof(int): " << alignof(int) << std::endl;
std::cout << "alignof(double): " << alignof(double) << std::endl;

一个复杂对象的内存布局可能受到填充(padding)和对齐(alignment)的影响:

struct Padded {char c;    // 1字节// 可能有填充int i;     // 4字节// 可能有填充double d;  // 8字节
};std::cout << "sizeof(Padded): " << sizeof(Padded) << std::endl;
// 可能大于13(1+4+8)字节,由于内存对齐

常见的new/delete问题与陷阱

内存泄漏

内存泄漏发生在动态分配的内存未被释放时:

void memoryLeak() {int* ptr = new int[100];// 无匹配的delete[],内存泄漏
}

悬挂指针(野指针)

悬挂指针指向已释放的内存:

int* createAndDestroy() {int* ptr = new int(42);delete ptr;  // 内存已释放return ptr;  // 返回悬挂指针
}int main() {int* dangling = createAndDestroy();*dangling = 10;  // 未定义行为:写入已释放的内存return 0;
}

重复释放

重复释放同一内存会导致未定义行为:

int* ptr = new int;
delete ptr;   // 第一次释放,正确
delete ptr;   // 第二次释放,未定义行为

使用错误的释放形式

不匹配的分配和释放形式会导致未定义行为:

int* single = new int;
delete[] single;  // 错误:应使用deleteint* array = new int[10];
delete array;     // 错误:应使用delete[]

自定义析构函数前提下的释放问题

如果基类没有虚析构函数,通过基类指针删除派生类对象会导致未定义行为:

class Base {
public:~Base() {  // 非虚析构函数std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* ptr = new Derived;delete ptr;  // 只调用Base析构函数,不调用Derived析构函数return 0;
}

std::nothrow的限制

std::nothrow只影响new的内存分配部分,构造函数仍然可能抛出异常:

class ThrowingCtor {
public:ThrowingCtor() {throw std::runtime_error("Constructor failed");}
};int main() {try {// nothrow只对内存分配部分起作用,构造函数异常仍会传播ThrowingCtor* obj = new(std::nothrow) ThrowingCtor;} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;}return 0;
}

替代方案与现代C++实践

智能指针

现代C++提供了智能指针,自动管理动态内存的生命周期:

#include <memory>// 独占所有权
std::unique_ptr<int> p1(new int(42));
// 或更好的方式
std::unique_ptr<int> p2 = std::make_unique<int>(42);  // C++14// 共享所有权
std::shared_ptr<int> p3 = std::make_shared<int>(42);
std::shared_ptr<int> p4 = p3;  // 现在p3和p4共享同一个对象

智能指针会在超出作用域时自动释放其管理的内存,无需显式调用delete

容器

标准库容器如std::vectorstd::list等自动管理内存:

std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {vec.push_back(i);  // 自动扩容,无需手动内存管理
}

RAII(资源获取即初始化)原则

使用RAII原则设计类,在构造函数中获取资源,在析构函数中释放资源:

class ResourceWrapper {
private:Resource* resource;public:ResourceWrapper(const std::string& resourceName) {resource = acquireResource(resourceName);}~ResourceWrapper() {releaseResource(resource);}// 禁止复制ResourceWrapper(const ResourceWrapper&) = delete;ResourceWrapper& operator=(const ResourceWrapper&) = delete;// 允许移动ResourceWrapper(ResourceWrapper&& other) noexcept : resource(other.resource) {other.resource = nullptr;}ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}// 使用资源的方法...
};

内存池和分配器

对于性能关键的应用,可以使用自定义内存池来优化频繁的小内存分配:

#include <cstddef>
#include <new>class SimpleMemoryPool {
private:struct Block {Block* next;};Block* freeList;static const size_t BLOCK_SIZE = 1024;public:SimpleMemoryPool() : freeList(nullptr) {}~SimpleMemoryPool() {Block* block = freeList;while (block) {Block* next = block->next;std::free(block);block = next;}}void* allocate(size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {return std::malloc(size);}if (!freeList) {// 分配一批新块char* memory = reinterpret_cast<char*>(std::malloc(BLOCK_SIZE * 10));if (!memory) return nullptr;// 将新内存块链接到自由列表for (int i = 0; i < 10; ++i) {Block* block = reinterpret_cast<Block*>(memory + i * BLOCK_SIZE);block->next = freeList;freeList = block;}}// 使用自由列表的第一个块Block* block = freeList;freeList = block->next;return block;}void deallocate(void* ptr, size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {std::free(ptr);return;}// 将块添加回自由列表Block* block = reinterpret_cast<Block*>(ptr);block->next = freeList;freeList = block;}
};// 为特定类型实现自定义new和delete
class MyObject {
private:static SimpleMemoryPool pool;public:void* operator new(size_t size) {return pool.allocate(size);}void operator delete(void* ptr, size_t size) {pool.deallocate(ptr, size);}// 类的其他成员...
};SimpleMemoryPool MyObject::pool;

性能考量

new/delete的开销

动态内存分配涉及多种开销:

  1. 调用操作系统分配内存:通常需要系统调用,这是昂贵的操作
  2. 查找合适的内存块:内存管理器需要查找足够大的空闲块
  3. 记录分配信息:存储元数据(如分配大小)
  4. 对齐处理:确保内存对齐
  5. 构造和析构:调用构造函数和析构函数

对于频繁的小内存分配和释放,这些开销可能会显著影响性能。

性能优化策略

可以采用以下策略优化动态内存管理:

  1. 减少分配次数:预分配、重用对象
  2. 批量分配:一次分配多个对象
  3. 使用内存池:为特定大小的对象预分配内存
  4. 避免碎片化:使用适当的分配策略
  5. 考虑替代方案:使用栈内存(当对象较小且生命周期有限时)

栈与堆的性能对比

栈分配通常比堆分配快得多:

#include <iostream>
#include <chrono>
#include <vector>const int ITERATIONS = 1000000;void stackAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int array[10];  // 栈分配array[0] = i;}
}void heapAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int* array = new int[10];  // 堆分配array[0] = i;delete[] array;}
}void vectorAllocation() {for (int i = 0; i < ITERATIONS; ++i) {std::vector<int> vec(10);  // 使用std::vectorvec[0] = i;}
}template<typename Func>
double measureTime(Func func) {auto start = std::chrono::high_resolution_clock::now();func();auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> duration = end - start;return duration.count();
}int main() {std::cout << "Stack allocation: " << measureTime(stackAllocation) << " ms" << std::endl;std::cout << "Heap allocation: " << measureTime(heapAllocation) << " ms" << std::endl;std::cout << "Vector allocation: " << measureTime(vectorAllocation) << " ms" << std::endl;return 0;
}

运行此代码会明显看出栈分配比堆分配快得多,而std::vector通常介于两者之间。

实际应用案例

实现简单的字符串类

下面是使用动态内存管理实现简单字符串类的示例:

#include <iostream>
#include <cstring>
#include <algorithm>class SimpleString {
private:char* data;size_t length;// 确保有足够的空间void ensureCapacity(size_t newLength) {if (newLength > length) {// 分配新内存(添加一些额外空间以减少重新分配次数)size_t newCapacity = std::max(newLength, length * 2);char* newData = new char[newCapacity + 1];  // +1 用于空终止符// 复制现有数据if (data) {std::strcpy(newData, data);delete[] data;}data = newData;length = newCapacity;}}public:// 默认构造函数SimpleString() : data(nullptr), length(0) {data = new char[1];data[0] = '\0';}// 从C风格字符串构造SimpleString(const char* str) : data(nullptr), length(0) {if (!str) {data = new char[1];data[0] = '\0';} else {length = std::strlen(str);data = new char[length + 1];std::strcpy(data, str);}}// 复制构造函数SimpleString(const SimpleString& other) : data(nullptr), length(0) {length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}// 移动构造函数SimpleString(SimpleString&& other) noexcept : data(other.data), length(other.length) {other.data = nullptr;other.length = 0;}// 析构函数~SimpleString() {delete[] data;}// 复制赋值SimpleString& operator=(const SimpleString& other) {if (this != &other) {delete[] data;length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}return *this;}// 移动赋值SimpleString& operator=(SimpleString&& other) noexcept {if (this != &other) {delete[] data;data = other.data;length = other.length;other.data = nullptr;other.length = 0;}return *this;}// 连接操作符SimpleString operator+(const SimpleString& other) const {SimpleString result;result.ensureCapacity(length + other.length);std::strcpy(result.data, data);std::strcat(result.data, other.data);return result;}// 获取C风格字符串const char* c_str() const {return data;}// 获取长度size_t size() const {return std::strlen(data);}// 访问元素char& operator[](size_t index) {return data[index];}const char& operator[](size_t index) const {return data[index];}// 设置新值void assign(const char* str) {if (!str) return;size_t newLength = std::strlen(str);ensureCapacity(newLength);std::strcpy(data, str);}// 追加字符串void append(const char* str) {if (!str) return;size_t currentLength = std::strlen(data);size_t appendLength = std::strlen(str);ensureCapacity(currentLength + appendLength);std::strcat(data, str);}
};int main() {// 测试构造函数SimpleString s1("Hello");SimpleString s2(" World");// 测试连接SimpleString s3 = s1 + s2;std::cout << s3.c_str() << std::endl;  // 输出:Hello World// 测试复制和赋值SimpleString s4 = s1;std::cout << s4.c_str() << std::endl;  // 输出:Hellos4 = s2;std::cout << s4.c_str() << std::endl;  // 输出:World// 测试修改s4[0] = 'w';  // 改为小写std::cout << s4.c_str() << std::endl;  // 输出:world// 测试appends4.append("!");std::cout << s4.c_str() << std::endl;  // 输出:world!return 0;
}

这个例子展示了如何使用newdelete在堆上管理字符串数据,包括深拷贝、移动语义和内存重分配逻辑。

自定义对象池

对于需要频繁创建和销毁的小对象,可以实现对象池来提高性能:

#include <iostream>
#include <vector>
#include <memory>template<typename T, size_t BlockSize = 100>
class ObjectPool {
private:// 表示内存块的结构struct Block {union {T value;Block* next;};// 禁用构造和析构函数以允许在union中使用TBlock() : next(nullptr) {}~Block() {}};Block* freeList;std::vector<std::unique_ptr<Block[]>> blocks;public:ObjectPool() : freeList(nullptr) {}~ObjectPool() {// blocks的vector会自动清理分配的内存}// 分配对象template<typename... Args>T* allocate(Args&&... args) {if (freeList == nullptr) {// 分配新的内存块auto newBlock = std::make_unique<Block[]>(BlockSize);// 初始化自由列表for (size_t i = 0; i < BlockSize - 1; ++i) {newBlock[i].next = &newBlock[i + 1];}newBlock[BlockSize - 1].next = nullptr;freeList = &newBlock[0];blocks.push_back(std::move(newBlock));}// 使用自由列表的第一个块Block* block = freeList;freeList = block->next;// 使用placement new构造对象return new (&block->value) T(std::forward<Args>(args)...);}// 释放对象void deallocate(T* object) {if (!object) return;// 调用析构函数object->~T();// 将块添加回自由列表Block* block = reinterpret_cast<Block*>(object);block->next = freeList;freeList = block;}
};// 测试用的类
class TestObject {
private:int id;public:TestObject(int i) : id(i) {std::cout << "TestObject " << id << " constructed." << std::endl;}~TestObject() {std::cout << "TestObject " << id << " destructed." << std::endl;}int getId() const { return id; }
};int main() {// 创建对象池ObjectPool<TestObject, 5> pool;// 分配一些对象std::vector<TestObject*> objects;for (int i = 0; i < 10; ++i) {objects.push_back(pool.allocate(i));}// 使用对象for (auto obj : objects) {std::cout << "Object ID: " << obj->getId() << std::endl;}// 释放一些对象for (int i = 0; i < 5; ++i) {pool.deallocate(objects[i]);objects[i] = nullptr;}// 分配更多对象(会重用已释放的内存)for (int i = 0; i < 5; ++i) {objects[i] = pool.allocate(i + 100);}// 清理所有对象for (auto obj : objects) {if (obj) {pool.deallocate(obj);}}return 0;
}

对象池可以显著减少内存分配和释放的开销,特别是对于频繁创建和销毁的小对象。

最佳实践

总结一些使用newdelete的最佳实践:

  1. 避免裸指针:尽可能使用智能指针(std::unique_ptrstd::shared_ptr

    // 不推荐
    Resource* res = new Resource();
    // 使用后...
    delete res;// 推荐
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    // 自动管理生命周期
    
  2. 优先使用标准库容器:它们已经在内部处理好内存管理

    // 不推荐
    int* array = new int[size];
    // 使用后...
    delete[] array;// 推荐
    std::vector<int> array(size);
    
  3. 确保匹配newdeletenew[]delete[]

    // 正确
    int* single = new int;
    delete single;int* array = new int[10];
    delete[] array;
    
  4. 遵循RAII原则:在构造函数中获取资源,在析构函数中释放资源

  5. 检查内存分配失败:使用异常处理或std::nothrow

    // 使用异常
    try {int* array = new int[1000000000];
    } catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }// 使用nothrow
    int* array = new(std::nothrow) int[1000000000];
    if (!array) {std::cerr << "Memory allocation failed" << std::endl;
    }
    
  6. 基类使用虚析构函数:确保通过基类指针正确删除派生类对象

    class Base {
    public:virtual ~Base() = default;
    };
    
  7. 考虑移动语义:当涉及大量数据转移时

    class MovableResource {
    public:// 移动构造函数MovableResource(MovableResource&& other) noexcept: data(other.data) {other.data = nullptr;}// 移动赋值运算符MovableResource& operator=(MovableResource&& other) noexcept {if (this != &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;}private:int* data;
    };
    
  8. 使用工具检测内存问题:Valgrind、AddressSanitizer等

总结

newdelete操作符是C++中动态内存管理的基本工具,它们允许程序员在运行时分配和释放内存。理解它们的工作原理、正确用法和潜在问题对于编写健壮的C++程序至关重要。

然而,在现代C++中,我们通常应该倾向于使用更高级的抽象(如智能指针、容器和RAII类)来自动管理内存,这有助于减少常见错误如内存泄漏、悬空指针和重复释放。

关键记忆点:

  • new分配内存并调用构造函数,delete调用析构函数并释放内存
  • 数组使用new[]分配,delete[]释放
  • 裸指针管理的内存需要显式释放,否则会导致内存泄漏
  • 现代C++提供更安全的替代方案:智能指针、容器和RAII

在下一篇文章中,我们将讨论内存泄漏的检测与避免,这是C++内存管理中的一个重要主题。


这是我C++学习之旅系列的第十七篇技术文章。查看完整系列目录了解更多内容。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/77239.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

15~30K,3年以上golang开发经验

继续分享最新的面经&#xff0c;前面发的两篇大家也可以看看&#xff1a; 「坐标上海&#xff0c;20K的面试强度」「北京七猫&#xff0c;薪资25~35K&#xff0c;瞧瞧面试强度」 今天分享的是golang开发岗面经&#xff0c;要求是3年以上golang开发经验&#xff0c;薪资为15~3…

Python爬虫实战:获取优志愿专业数据

一、引言 在信息爆炸的当下,数据成为推动各领域发展的关键因素。优志愿网站汇聚了丰富的专业数据,对于教育研究、职业规划等领域具有重要价值。然而,为保护自身数据和资源,许多网站设置了各类反爬机制。因此,如何高效、稳定地从优志愿网站获取计算机专业数据成为一个具有…

ArcPy工具箱制作(下)

在上一篇博客中&#xff0c;我们已经初步了解了如何制作ArcPy工具箱&#xff0c;包括工具箱的基本概念、准备工作、脚本编写以及将脚本转换为工具箱的步骤。今天&#xff0c;我们将继续深入探讨ArcPy工具箱的制作&#xff0c;重点介绍一些进阶技巧和优化方法. 一、优化工具箱的…

不一样的flag 1(迷宫题)

题目 做法 下载压缩包&#xff0c;解压&#xff0c;把解压后的文件拖进Exeinfo PE进行分析 32位&#xff0c;无壳 扔进IDA&#xff08;32位&#xff09;&#xff0c;找到main&#xff0c;F5反编译 没啥关键词&#xff0c;ShiftF12也找不到什么有用的点 从上往下分析吧 puts(…

工程化实践:Flutter项目结构与规范

工程化实践&#xff1a;Flutter项目结构与规范 在Flutter项目开发中&#xff0c;良好的工程化实践对于提高开发效率、保证代码质量和团队协作至关重要。本文将从项目结构、代码规范、CI/CD流程搭建以及包管理等方面&#xff0c;详细介绍Flutter项目的工程化最佳实践。 项目结…

[Java · 初窥门径] Java 语言初识

&#x1f31f; 想系统化学习 Java 编程&#xff1f;看看这个&#xff1a;[编程基础] Java 学习手册 0x01&#xff1a;Java 编程语言简介 Java 是一种高级计算机编程语言&#xff0c;它是由 Sun Microsystems 公司&#xff08;已被 Oracle 公司收购&#xff09;于 1995 年 5 …

1187. 【动态规划】竞赛总分

题目描述 学生在我们USACO的竞赛中的得分越多我们越高兴。我们试着设计我们的竞赛以便人们能尽可能的多得分。 现在要进行一次竞赛&#xff0c;总时间T固定&#xff0c;有若干类型可选择的题目&#xff0c;每种类型题目可选入的数量不限&#xff0c;每种类型题目有一个si(解答…

使用KeilAssistant代替keil的UI界面

目录 一、keil Assistant的优势和缺点 二、使用方法 &#xff08;1&#xff09;配置keil的路径 &#xff08;2&#xff09;导入并使用工程 &#xff08;3&#xff09;默认使用keil自带的ARM编译器而非GUN工具链 一、keil Assistant的优势和缺点 在日常学…

【React】通过 fetch 发起请求,设置 proxy 处理跨域

fetch 基本使用跨域处理 fetch 基本使用 在node使用原生ajax发请求&#xff1a;XMLHttpRequest()1.获取xhr对象 2.注册回调函数 3.设置参数&#xff0c;请求头 4.发起连接原生ajax没有带异步处理 promise&#xff1b;原生ajax封装一下&#xff0c;以便重复调用jQuery&#…

Redis(二) - Redis命令详解

文章目录 前言一、启动Redis并进入客户端1. 启动Redis2. 进入Redis客户端3. 使用IDEA连接Redis 二、查看命令帮助信息1. 查看所有命令2. 查看指定命令帮助 三、键操作命令1. set命令2. mset命令3. keys命令4. get命令5. mget命令6. dump命令7. exists命令8. type命令9. rename命…

【Qt】初识Qt(二)

目录 一、显示hello world1.1 图形化界面1.2 写代码 二、对象树三、使用输入框显示hello world四、使用按钮显示hello world 一、显示hello world 有两种方式实现hello world&#xff1a; 通过图形化界面&#xff0c;在界面上创建出一个控件&#xff0c;显示hello world通过写…

空调制冷量和功率有什么关系?

空调的制冷量和功率是衡量空调性能的两个核心参数,二者既有区别又紧密相关,以下是具体解析: 1. 基本定义 制冷量(Cooling Capacity)指空调在单位时间内从室内环境中移除的热量,单位为 瓦特(W) 或 千卡/小时(kcal/h)。它直接反映空调的制冷能力,数值越大,制冷效果越…

【prometheus+Grafana篇】Prometheus与Grafana:深入了解监控架构与数据可视化分析平台

&#x1f4ab;《博主主页》&#xff1a;奈斯DB-CSDN博客 &#x1f525;《擅长领域》&#xff1a;擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(MongoDB)有了解 &#x1f496;如果觉得文章对你有所帮…

基于n8n的AI应用工作流原理与技术解析

基于n8n的AI应用工作流原理与技术解析 在AI技术深度融入企业数字化转型的今天&#xff0c;开源工作流自动化工具n8n凭借其灵活的架构和强大的集成能力&#xff0c;成为构建智能自动化流程的核心引擎。本文将从技术原理、AI融合机制、典型应用场景三个维度&#xff0c;解析n8n在…

经济指标学习(二)

系列文章目录 文章目录 系列文章目录1、市净率**一、定义与计算****二、核心意义****三、应用场景****四、局限性****五、分类与衍生指标****总结** 2、市销率**一、定义与计算****二、核心意义****三、优缺点分析****四、适用场景****五、与其他指标的对比****六、实际应用案例…

大语言模型减少幻觉的常见方案

什么是大语言模型的幻觉 大语言模型的幻觉&#xff08;Hallucination&#xff09;是指模型在生成文本时&#xff0c;输出与输入无关、不符合事实、逻辑错误或完全虚构的内容。这种现象主要源于模型基于概率生成文本的本质&#xff0c;其目标是生成语法合理、上下文连贯的文本&…

CSS 美化页面(四)

一、浮动float属性 ‌属性值‌‌描述‌‌适用场景‌left元素向左浮动&#xff0c;腾出右侧空间供其他元素使用&#xff0c;其他内容会围绕在其右侧‌。横向排列元素&#xff08;如导航菜单&#xff09;、图文混排布局‌。right元素向右浮动&#xff0c;腾出左侧空间供其他元素使…

如何将 .txt 文件转换成 .md 文件

一、因为有些软件上传文件的时候需要 .md 文件&#xff0c;首先在文件所在的目录中&#xff0c;点击“查看”&#xff0c;然后勾选上“文件扩展名”&#xff0c;这个时候该目录下的所有文件都会显示其文件类型了。 二、这时直接对目标的 .txt 文件进行重命名&#xff0c;把后缀…

C++ 迭代器失效详解:如何避免 vector 操作中的陷阱

目录 1. 什么是迭代器失效&#xff1f; 2. 哪些操作会导致迭代器失效&#xff1f; 2.1 vector 的插入操作&#xff08;push_back, insert&#xff09; 示例&#xff1a;push_back 导致迭代器失效 如何避免&#xff1f; 2.2 vector 的删除操作&#xff08;erase, pop_back&…

(EtherCAT 转 EtherNet/IP)EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

型号 协议转换通信网关 EtherCAT 转 EtherNet/IP MS-GW12 概述 MS-GW12 是 EtherCAT 和 EtherNet/IP 协议转换网关&#xff0c;为用户提供两种不同通讯协议的 PLC 进行数据交互的解决方案&#xff0c;可以轻松容易将 EtherNet/IP 网络接入 EtherCAT 网络中&#xff0c;方便…