C++学习:C++类型转换专栏

对于C/C++这种强类型语言而言,类型转换是常态。

但是对于C++而言,类型转换又比C语言麻烦一些。

本期我们就来简单学习一下C++类型转换相关的知识

相关代码已经上传至作者个人gitee:楼田莉子/CPP代码学习喜欢请支持一下谢谢

目录

C语言类型转换

C++的类型转换

C++头文件

核心组件概览

头文件中常见函数的主要作用

[智能指针(Smart Pointers)](#智能指针(Smart Pointers))

[std::unique_ptr - 独占所有权指针](#std::unique_ptr - 独占所有权指针)

[std::shared_ptr - 共享所有权指针](#std::shared_ptr - 共享所有权指针)

[std::weak_ptr - 弱引用指针](#std::weak_ptr - 弱引用指针)

内存分配器(Allocators)

自定义内存分配

[工具函数(Utility Functions)](#工具函数(Utility Functions))

[std::addressof - 获取对象地址](#std::addressof - 获取对象地址)

[std::align - 内存对齐](#std::align - 内存对齐)

工厂函数

[std::make_unique 和 std::make_shared](#std::make_unique 和 std::make_shared)

智能指针对比总结

应用场景

资源管理(RAII)

避免循环引用

C++中的强制显式类型转换

内存安全

C++中4种强制类型转换运算符

RAII


C语言类型转换

在C语⾔中,如果赋值运算符左右两侧类型不同,或者形参与实参类型不匹配,或者返回值类型与 接收返回值类型不⼀致时等场景,就需要发⽣类型转化。

C语⾔中总共有两种形式的类型转换:隐 式类型转换和显式强制类型转换。

隐式类型转化:编译器在编译阶段⾃动进⾏,能转就转,不能转就编译失败

显式强制类型转化:需要⽤⼾⾃去显⽰在变量前⽤括号指定要转换的类型。

并不是任意类型之前都⽀持转换,两个类型⽀持转换需要有⼀定关联性,也就是说转换后要有⼀定 的意义,两个毫⽆关联的类型是不⽀持转换的。

举例说明:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int i = 1;
	// 隐式类型转换

	// 隐式类型转换主要发⽣在整形和整形之间,整形和浮点数之间,浮点数和浮点数之间

	double d = i;
	printf("%d, %.2f\n", i, d);
	int* p = &i;
	// 显⽰的强制类型转换

	// 强制类型转换主要发⽣在指针和整形之间,指针和指针之间

	int address = (int)p;
	printf("%p, %d\n", p, address);
	// malloc返回值是void*,被强转成int*
	int* ptr = (int*)malloc(8);
	// 编译报错:类型强制转换:⽆法从"int* "转换为"double"
	// 指针是地址的编号,也是⼀种整数,所以可以和整形互相转换

	// 但是指针和浮点数毫⽆关联,强转也是不⽀持的

	// d = (double)p;
	return 0;
}

C++的类型转换

C++兼容C,所以C⽀持的隐式类型转换和显式强制类型转换C++都⽀持。

单参数类型转换。下图所示的为隐式类型转换

C++还⽀持内置类型到⾃定义类型之间的转换,内置类型转成⾃定义类型需要构造函数的⽀持,⾃定义类型转成内置类型,需要⼀个operator类型()的函数去⽀持。

C++还⽀持⾃定义类型到⾃定义类型之间的转换,需要对应类型的构造函数⽀持即可,⽐如A类型对 象想转成B类型,则⽀持⼀个形参为A类型的B构造函数即可⽀持。

自定义类型转换示例:

cpp 复制代码
//test.cpp
#include"list.h"
using namespace std;
void test2()
{
	Boogiepop::list<int> lt = { 1,2,3,4,5 };

	//权限缩⼩?权限缩⼩和放⼤,仅限于const的指针和引⽤
	//这⾥不是权限缩⼩,这⾥是⾃定义类型=⾃定义类型之间的类型转换
	//具体实现看下⾯ListIterator中对应的构造函数的实现

	Boogiepop::list<int>::const_iterator cit = lt.begin();
	while (cit != lt.end())
	{
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
}
int main()
{
	test2();
	return 0;
}
//list.h
#pragma once
#include <assert.h>
#include <initializer_list>
using namespace std;
namespace Boogiepop
{
    template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;
        ListNode(const T& data = T())
            :_next(nullptr)
            , _prev(nullptr)
            , _data(data)
        {
        }
    };

    template<class T, class Ref, class Ptr>
    struct ListIterator
    {
        typedef ListNode<T> Node;
        typedef ListIterator<T, Ref, Ptr> Self;
        Node* _node;

        ListIterator(Node* node)
            :_node(node)
        {
        }

        // typedef ListIterator<T, T&, T*> iterator;
        // typedef ListIterator<T, const T&, const T*> const_iterator;
        // ListIterator实例化为iterator时,这个函数是拷⻉构造
        // ListIterator实例化为const_iterator时,
        // 这个函数⽀持iterator转换为const_iterator构造函数
        ListIterator(const ListIterator<T, T&, T*>& it)
            :_node(it._node)
        {
        }

        // ++it;
        Self& operator++()
        {
            _node = _node->_next;
            return *this;
        }

        // --it;
        Self& operator--()
        {
            _node = _node->_prev;
            return *this;
        }

        // it++
        Self operator++(int)
        {
            Self tmp(*this);
            _node = _node->_next;
            return tmp;
        }

        // 修正2:后置递减运算符返回Self而不是Self&
        Self operator--(int)
        {
            Self tmp(*this);
            _node = _node->_prev;
            return tmp;
        }

        Ref operator*()
        {
            return _node->_data;
        }

        Ptr operator->()
        {
            return &_node->_data;
        }

        bool operator!=(const Self& it)
        {
            return _node != it._node;
        }

        bool operator==(const Self& it)
        {
            return _node == it._node;
        }
    };

    template<class T>
    class list
    {
        typedef ListNode<T> Node;
    public:
        //同⼀个类模板给不同参数会实例化出不同的类型
        typedef ListIterator<T, T&, T*> iterator;
        typedef ListIterator<T, const T&, const T*> const_iterator;

        iterator begin()
        {
            return iterator(_head->_next);
        }

        const_iterator begin() const
        {
            return const_iterator(_head->_next);
        }

        iterator end()
        {
            return iterator(_head);
        }

        const_iterator end() const
        {
            return const_iterator(_head);
        }

        void empty_init()
        {
            _head = new Node();
            _head->_next = _head;
            _head->_prev = _head;
        }

        list()
        {
            empty_init();
        }

        // 修正3:添加std::命名空间限定
        list(std::initializer_list<T> il)
        {
            empty_init();
            for (const auto& e : il)
            {
                push_back(e);
            }
        }

        void push_back(const T& x)
        {
            insert(end(), x);
        }

        // 没有iterator失效
        iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(x);
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }

    private:
        Node* _head;
    };
}

C++头文件<memory>

<memory> 头文件是 C++ 标准库中用于智能内存管理的核心头文件,提供了自动内存管理的工具,避免手动内存管理带来的问题。

核心组件概览

组件 功能 特点
智能指针 自动内存管理 防止内存泄漏,自动释放资源
内存分配器 自定义内存分配 控制内存分配策略
工具函数 内存操作辅助 提供安全的内存操作工具

<memory> 头文件中常见函数的主要作用

函数名 主要作用 引入版本 特点 使用示例
std::make_unique 创建 std::unique_ptr 智能指针 C++14 异常安全,避免直接使用 new auto ptr = make_unique<MyClass>(args);
std::make_shared 创建 std::shared_ptr 智能指针 C++11 单次内存分配,效率更高 auto ptr = make_shared<MyClass>(args);
std::make_shared_for_overwrite 创建未初始化的 shared_ptr C++20 避免值初始化开销 auto ptr = make_shared_for_overwrite<T>();
std::make_unique_for_overwrite 创建未初始化的 unique_ptr C++20 避免值初始化开销 auto ptr = make_unique_for_overwrite<T>();
std::allocate_shared 使用自定义分配器创建 shared_ptr C++11 控制内存分配策略 auto ptr = allocate_shared<T>(alloc, args);
std::addressof 获取对象的真实地址 C++11 绕过 operator& 重载 T* p = addressof(obj);
std::align 内存对齐调整 C++11 在缓冲区中对齐指针 align(alignment, size, ptr, space);

智能指针(Smart Pointers)

std::unique_ptr - 独占所有权指针

cpp 复制代码
#include <memory>
#include <iostream>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "MyClass constructed: " << data << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed: " << data << std::endl;
    }
    void print() { std::cout << "Value: " << data << std::endl; }
private:
    int data;
};

void unique_ptr_example() {
    // 创建 unique_ptr
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>(42);
    ptr1->print();
    
    // 所有权转移(移动语义)
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
    
    if (!ptr1) {
        std::cout << "ptr1 现在为空" << std::endl;
    }
    ptr2->print();
    
    // 自动释放内存,无需手动 delete
}

std::shared_ptr - 共享所有权指针

cpp 复制代码
#include <memory>

void shared_ptr_example() {
    // 创建 shared_ptr
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(100);
    
    {
        std::shared_ptr<MyClass> ptr2 = ptr1;  // 共享所有权
        std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 2
        
        ptr2->print();
    }  // ptr2 离开作用域,引用计数减1
    
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 1
    ptr1->print();
    
    // 当最后一个 shared_ptr 销毁时,对象自动释放
}

std::weak_ptr - 弱引用指针

cpp 复制代码
#include <memory>

void weak_ptr_example() {
    std::shared_ptr<MyClass> shared = std::make_shared<MyClass>(200);
    std::weak_ptr<MyClass> weak = shared;
    
    std::cout << "引用计数: " << shared.use_count() << std::endl;  // 1
    
    // 使用 weak_ptr 前需要转换为 shared_ptr
    if (auto temp = weak.lock()) {
        temp->print();  // 对象还存在
    } else {
        std::cout << "对象已被释放" << std::endl;
    }
    
    shared.reset();  // 释放对象
    
    if (auto temp = weak.lock()) {
        temp->print();
    } else {
        std::cout << "对象已被释放" << std::endl;  // 这里会执行
    }
}

内存分配器(Allocators)

自定义内存分配

cpp 复制代码
#include <memory>
#include <vector>

template<typename T>
class CustomAllocator {
public:
    using value_type = T;
    
    CustomAllocator() = default;
    
    template<typename U>
    constexpr CustomAllocator(const CustomAllocator<U>&) noexcept {}
    
    T* allocate(std::size_t n) {
        std::cout << "分配 " << n << " 个元素" << std::endl;
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    
    void deallocate(T* p, std::size_t n) noexcept {
        std::cout << "释放 " << n << " 个元素" << std::endl;
        ::operator delete(p);
    }
};

void allocator_example() {
    // 使用自定义分配器的 vector
    std::vector<int, CustomAllocator<int>> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
}

工具函数(Utility Functions)

std::addressof - 获取对象地址

cpp 复制代码
#include <memory>

class WeirdClass {
public:
    WeirdClass* operator&() {  // 重载了 & 运算符
        return nullptr;
    }
};

void addressof_example() {
    WeirdClass obj;
    
    WeirdClass* p1 = &obj;           // 使用重载的 &,得到 nullptr
    WeirdClass* p2 = std::addressof(obj);  // 得到真实地址
    
    std::cout << "重载&: " << p1 << std::endl;      // 0
    std::cout << "addressof: " << p2 << std::endl;  // 真实地址
}

std::align - 内存对齐

cpp 复制代码
#include <memory>

void align_example() {
    char buffer[100];
    std::size_t space = sizeof(buffer);
    
    void* ptr = buffer;
    
    // 尝试在 buffer 中寻找对齐到 16 字节边界的地址
    if (std::align(16, 50, ptr, space)) {
        std::cout << "对齐成功: " << ptr << std::endl;
        std::cout << "剩余空间: " << space << std::endl;
    }
}

工厂函数

std::make_uniquestd::make_shared

cpp 复制代码
#include <memory>

class Resource {
public:
    Resource(int a, int b, int c) 
        : x(a), y(b), z(c) {}
    
    void show() {
        std::cout << "Resource: " << x << ", " << y << ", " << z << std::endl;
    }
private:
    int x, y, z;
};

void factory_functions_example() {
    // C++14 引入 make_unique
    auto unique = std::make_unique<Resource>(1, 2, 3);
    unique->show();
    
    // C++11 引入 make_shared(更高效,引用计数和对象在同一内存块)
    auto shared = std::make_shared<Resource>(4, 5, 6);
    shared->show();
    
    // 对比传统 new
    std::shared_ptr<Resource> old_way(new Resource(7, 8, 9));  // 不推荐
}

智能指针对比总结

特性 unique_ptr shared_ptr weak_ptr
所有权 独占 共享 无所有权
拷贝 不允许 允许 允许
移动 允许 允许 允许
引用计数 观察计数
性能 接近裸指针 有额外开销 有额外开销
适用场景 独占资源 共享资源 打破循环引用

应用场景

资源管理(RAII)

cpp 复制代码
#include <memory>
#include <fstream>

class FileHandler {
private:
    std::unique_ptr<std::fstream> file;
public:
    FileHandler(const std::string& filename) 
        : file(std::make_unique<std::fstream>(filename)) {
        if (!file->is_open()) {
            throw std::runtime_error("无法打开文件");
        }
    }
    // 自动关闭文件,无需手动调用 close()
};

void raii_example() {
    try {
        FileHandler handler("data.txt");
        // 使用文件...
    } catch (const std::exception& e) {
        std::cout << "错误: " << e.what() << std::endl;
    }
    // 文件自动关闭
}

避免循环引用

cpp 复制代码
#include <memory>

class Node {
public:
    std::string name;
    std::shared_ptr<Node> parent;
    std::weak_ptr<Node> child;  // 使用 weak_ptr 避免循环引用
    
    Node(const std::string& n) : name(n) {}
    ~Node() {
        std::cout << "销毁节点: " << name << std::endl;
    }
};

void cycle_reference_example() {
    auto node1 = std::make_shared<Node>("父节点");
    auto node2 = std::make_shared<Node>("子节点");
    
    node1->child = node2;  // 弱引用
    node2->parent = node1; // 强引用
    
    // 可以正常释放,不会内存泄漏
}

C++中的强制显式类型转换

内存安全

类型安全是指编程语⾔在编译和运⾏时提供保护机制,避免⾮法的类型转换和操作,导致出现⼀个 内存访问错误等,从⽽减少程序运⾏时的错误。

C语⾔不是类型安全的语⾔,C语⾔允许隐式类型转换,⼀些特殊情况下就会导致越界访问的内存错 误,其次不合理的使⽤强制类型转换也会导致问题,⽐如⼀个int*的指针强转成double*访问就会 出现越界。

C++兼容C语⾔,⽀持隐式类型转换和强制类型转换,C++也不是类型安全的语⾔,C++提出4个显 ⽰的命名强制类型转换

cpp 复制代码
static_cast/reinterpret_cast/const_cast/dynamic_cast

就是为了让类型转 换相对⽽⾔更安全。

cpp 复制代码
void insert(size_t pos, char ch)
{
	//这⾥当pos == 0时,就会引发由于隐式类型转换
	// end跟pos⽐较时,提升为size_t导致判断结束逻辑出现问题

	// 在数组中访问挪动数据就会出现越界,经典的类型安全问题

	int end = 10;
	while (end >= pos)
	{
		// ...
		cout << end << endl; --end;
	}
}
void test3()
{
	insert(5, 'x');
	//insert(0, 'x');
	// 	这⾥会本质已经出现了越界访问,只是越界不⼀定能被检查出来

	int x = 100;
	double* p1 = (double*)&x;
	cout << *p1 << endl;
	const int y = 0;//可以利用关键字volatile来避免优化
	int* p2 = (int*)&y;
	(*p2) = 1;
	// 	这⾥打印的结果是1和	0,也是因为我们类型转换去掉了const属性

	// 	但是编译器认为	y是const的,不会被改变,所以会优化编译时放到

	// 寄存器或者直接替换y为0导致的
	volatile const int z = 0;
	int* p3 = (int*)&z;
	(*p3) = 1;
	// 	这⾥打印的结果是1和	0,也是因为我们类型转换去掉了const属性
	cout << *p2 << endl;
	cout << y << endl;
	cout << *p3 << endl;
	cout << z << endl;
}

C++中4种强制类型转换运算符

static_cast⽤于两个类型意义相近的转换,这个转换是具有明确定义的,只要底层不包含const, 都可以使⽤static_cast

reinterpret_cast⽤于两个类型意义不相近的转换,reinterpret是重新解释的意思,通常为运算对 象的位模式提供较低层次上的重新解释,也就是说转换后对原有内存的访问解释已经完全改变了, ⾮常的⼤胆。所以我们要谨慎使⽤,清楚知道这样转换是没有内存访问安全问题的。

const_cast⽤于const类型到⾮const类型的转换,去掉了const属性,也是⼀样的我们要谨慎使 ⽤,否则可能会出现意想不到的结果。

dynamic_cast⽤于将基类的指针或者引⽤安全的转换成派⽣类的指针或者引⽤。如果基类的指针 或者引⽤时指向派⽣类对象的,则转换回派⽣类指针或者引⽤时可以成功的,如果基类的指针指向 基类对象,则转换失败返回nullptr,如果基类引⽤指向基类对象,则转换失败,抛出bad_cast异 常。

其次dynamic_cast要求基类必须是多态类型,也就是基类中必须有虚函数。因为dynamic_cast是 运⾏时通过虚表中存储的type_info判断基类指针指向的是基类对象还是派⽣类对象。

cpp 复制代码
void test1()
{
	// 对应隐式类型转换--数据的解释意义没有改变

	double d = 12.34;
	int a = static_cast<int>(d);
	cout << a << endl;
	int&& ref = static_cast<int&&>(a);
	// 对应强制类型转换--数据的解释意义已经发⽣改变
	int* p1 = reinterpret_cast<int*>(a);
	// 对应强制类型转换中有⻛险的去掉const属性
	// 所以要注意加volatile
	volatile const int b = 0;
	int* p2 = const_cast<int*>(&b);
	*p2 = 1;
	cout << b << endl;
	cout << *p2 << endl;
	// 对应类型名查询
	cout << typeid(d).name() << endl;
	cout<<typeid(a).name()<<endl;
	cout << typeid(ref).name() << endl;
	cout << typeid(p1).name() << endl;
	cout << typeid(p2).name() << endl;
	cout<<typeid(b).name()<<endl;
}

结果为:

cpp 复制代码
void fun1(A* pa)
{
	// 指向⽗类转换时有⻛险的,后续访问存在越界访问的⻛险

		// 指向⼦类转换时安全

	B* pb1 = (B*)pa;
	cout << "pb1:" << pb1 << endl;
	cout << pb1->_a << endl;
	cout << pb1->_b << endl;
	pb1->_a++;
	pb1->_b++;
	cout << pb1->_a << endl;
	cout << pb1->_b << endl;
}
void fun2(A* pa)
{
	// dynamic_cast会先检查是否能转换成功(指向⼦类对象),能成功则转换,
	// (指向⽗类对象)转换失败则返回nullptr
	B* pb1 = dynamic_cast<B*>(pa);
	if (pb1)
	{
		cout << "pb1:" << pb1 << endl;
		cout << pb1->_a << endl;
		cout << pb1->_b << endl;
		pb1->_a++;
		pb1->_b++;
		cout << pb1->_a << endl;
		cout << pb1->_b << endl;
	}
	else
	{
		cout << "转换失败" << endl;
	}
}
void fun3(A& pa)
{
	// 转换失败,则抛出bad_cast异常
	try 
	{
		B& pb1 = dynamic_cast<B&>(pa);
		cout << "转换成功" << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}
void test2()
{
	A a;
	B b;
	//fun1(&a);
	//fun1(&b);
	fun2(&a);
	fun2(&b);
	fun3(a);
	fun3(b);
}

RAII

RTTI的英⽂全称是"RuntimeTypeIdentification",中⽂称为"运⾏时类型识别",它指的是程序在 运⾏的时候才确定需要⽤到的对象是什么类型的。⽤于在运⾏时(⽽不是编译时)获取有关对象的 信息。

RTTI主要由两个运算符实现,typeid和dynamic_cast;typeid主要⽤于返回表达式的类型, dynamic_cast前⾯已经讲过了,主要⽤于将基类的指针或者引⽤安全的转换成派⽣类的指针或者 引⽤。

typeid(e)中e可以是任意表达式或类型的名字,typeid(e)的返回值是typeinfo或typeinfo派⽣类对 象的引⽤,typeinfo可以只⽀持⽐较等于和不等于,name成员函数可以返回C⻛格字符串表⽰对象 类型名字,typeinfo的精确定义随着编译器的不同⽽略有差异,也就以为着同⼀个e表达式,不同 编译器下,typeid(e).name()返回的名字可能是不⼀样的。

typeinfo的⽂档如下:<typeinfo> - C++参考

typeid(e)时,当运算对象不属于类类型或者是⼀个不包含任何虚函数的类时,typeid返回的是运算 对象的静态类型,当运算对象是定义了⾄少⼀个虚函数的类的左值时,typeid的返回结果直到运⾏ 时才会求得。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <vector>
#include <typeinfo>
#include <exception>
#include<list>
using namespace std;
int main()
{
	int a[10];
	int* ptr = nullptr;
	cout << typeid(10).name() << endl;
	cout << typeid(a).name() << endl;
	cout << typeid(ptr).name() << endl;
	cout << typeid(string).name() << endl;
	cout << typeid(string::iterator).name() << endl;
	cout << typeid(vector<int>).name() << endl;
	cout << typeid(vector<int>::iterator).name() << endl;
	return 0;
}

vs2022下结果为:

g++编辑器下运行结果为:

cpp 复制代码
using namespace std;
class A
{
public:
    virtual void func()
    {
    }
protected:
    int _a1 = 1;
};
class B : public A
{
protected:
    int _b1 = 2;
};
int main()
{
    try
    {
        B* pb = new B;
        A* pa = (A*)pb;
        if (typeid(*pb) == typeid(B))
        {
            cout << "typeid(*pb) == typeid(B)" << endl;
        }
        // 如果 A 和  B不是继承关系,则会抛 bad_typeid异常
        if (typeid(*pa) == typeid(B))
        {
            cout << "typeid(*pa) == typeid(B)" << endl;
        }
        // 这⾥ pa 和 pb是 A* 和 B*,不是类类型对象,他会被当做编译是求值的静态类型运算

        // 所以这⾥始终是不相等的

            if (typeid(pa) == typeid(pb))
            {
                cout << "typeid(pa) == typeid(B)" << endl;
            }
    }
    catch (const std::exception& e)
    {
        cout << e.what() << endl;
    }

    return 0;
}

结果为:

本期内容就到这里了,喜欢请点个赞谢谢

封面图自取:

相关推荐
David WangYang2 小时前
便宜的自制 30 MHz - 6 GHz 矢量网络分析仪
开发语言·网络·php
liujing102329292 小时前
Day03_STM32F103C8T6学习笔记6-9章(江科大)
笔记·stm32·学习
魔芋红茶2 小时前
RuoYi 学习笔记 3:二次开发
java·笔记·学习
AI绘画哇哒哒2 小时前
实战:SQL统一访问200+数据源,构建企业级智能检索与RAG系统(下)
人工智能·sql·深度学习·学习·ai·程序员·大模型
诗句藏于尽头2 小时前
关于七牛云OSS存储的图片数据批量下载到本地
开发语言·windows·python
楼田莉子2 小时前
C++IO流学习
开发语言·c++·windows·学习·visual studio
-雷阵雨-3 小时前
数据结构——包装类&&泛型
java·开发语言·数据结构·intellij-idea
江拥羡橙3 小时前
JavaScript异步编程:告别回调地狱,拥抱Promise async/await
开发语言·javascript·ecmascript·promise·async/await
轩情吖3 小时前
Qt常用控件之QComboBox
开发语言·c++·qt·控件·下拉框·qcombobox·桌面级开发