C++参数传递:值、指针与引用的原理与实战

在C++编程中,函数参数传递看似是基础操作,却藏着不少影响代码性能、安全性的关键细节 ------ 新手常困惑 "值传递为啥改不了原变量",老手也可能在 "指针 vs 引用" 的选择上踩坑。

其实这背后的核心,就是值传递、指针传递、引用传递三种机制的底层逻辑差异。

咱们先明确一个共性:无论哪种传递方式,调用函数时都会发生一次 "拷贝"------ 但拷贝的不是 "数据本身",就是 "地址",这也是三种方式最本质的区别。

比如值传递拷贝的是实参的完整数据,而指针 / 引用传递拷贝的是地址(或地址的绑定关系);尤其指针传递,很多人误以为 "形参和实参是同一个指针",实则形参是实参指针的副本,只是两者指向同一块内存而已(这一点也是理解二级指针的关键,后面咱们慢慢说)。

粉丝福利, 免费领取C/C++ 开发学习资料包、技术视频/项目代码,1000道大厂面试题,内容包括(Qt,音视频开发,C++基础,数据库,高性能网络,组件设计,中间件开发,框架,分布式架构,云原生等进阶学习资料和最佳学习路线)↓↓↓↓↓↓见下面↓↓文章底部关注领取↓↓

Part1 值传递

1.1、什么是值传递?

值传递是最基础的参数传递方式。核心是 "传递实参的完整副本"------ 当你调用一个值传递的函数时,编译器会在函数的栈帧里,为形参创建一个独立的内存空间,然后把实参的所有数据 "原封不动" 地拷贝到这个空间里。

举个具体例子:

咱们在main函数里定义int num = 5,num会存在main函数的栈帧中(假设地址是0x0012ff44);当调用increment(num)时,编译器会在increment函数的栈帧里,开辟一块新内存给形参x(比如地址0x0012ff00),然后把num的值(5)拷贝到x的内存里。

这就意味着:

函数内部操作的x,和main里的num是两块完全独立的内存------ 哪怕你把x改得面目全非,num也不会受任何影响;等函数执行结束,increment的栈帧被销毁,x的内存也会被释放,整个过程对原变量毫无副作用。

示例

复制代码
#include <iostream>
using namespace std;
// 形参x:在increment的栈帧中创建,是num的副本
void increment(int x) {
    x++;  // 仅修改x的内存(0x0012ff00),与num的内存(0x0012ff44)无关
    cout << "函数内x的值:" << x << ",x的地址:" << &x << endl;  // 输出6,地址0x0012ff00(示例)
}
int main() {
    int num = 5;  // num在main的栈帧中,地址假设为0x0012ff44
    cout << "函数外num初始值:" << num << ",num的地址:" << &num << endl;  // 输出5,地址0x0012ff44(示例)


    increment(num);  // 拷贝num的值到x,而非传递num的地址


    cout << "函数外num最终值:" << num << endl;  // 输出5,num的内存未被修改
    return 0;
}

1.2、值传递的底层原理

  • 内存分配:函数调用时会在栈上分配新空间存储形参副本。
  • 复制开销:对于基本类型(如int)几乎无开销,但对大型对象(如std::string)会触发构造函数和析构函数调用。

1.3、值传递的 3 个关键特点

1)、安全性拉满,但拷贝开销是硬伤

值传递的 "独立性" 是最大优势 ------ 函数再怎么操作形参,都不会影响原始数据,完全符合 "无副作用函数" 的设计原则,特别适合处理敏感数据(比如用户密码、配置参数)。但问题在于 "拷贝开销":

  • 如果传递的是int、float这类基本类型(通常 4~8 字节),拷贝速度极快,开销可忽略;
  • 但如果传递的是大型对象(比如包含 1000 个元素的std::vector、有多个成员的复杂类),拷贝就需要复制所有成员数据 ------ 比如一个vector<int> vec(1000, 1),拷贝时要复制 1000 个int(共 4000 字节),如果频繁调用这个函数,性能损耗会非常明显。

2)、会触发对象的拷贝构造函数

对于自定义类对象,值传递不仅拷贝成员数据,还会显式调用拷贝构造函数(如果用户没定义,编译器会生成默认拷贝构造)。这一点很容易被忽略,比如:

复制代码
class MyClass {
public:
    MyClass() { cout << "默认构造" << endl; }
    // 拷贝构造函数
    MyClass(const MyClass& other) { cout << "拷贝构造" << endl; }
};
void func(MyClass obj) {}  // 值传递
int main() {
    MyClass a;  // 输出"默认构造"
    func(a);    // 输出"拷贝构造"(拷贝a到obj)
    return 0;
}

如果MyClass的拷贝构造函数里有复杂逻辑(比如深拷贝动态内存),值传递的开销会进一步增大。

3)、形参的生命周期独立于实参

形参只在函数执行期间存在(位于函数栈帧),函数结束后会自动销毁(调用析构函数,若有),不会和实参的生命周期产生关联。这一点比引用 / 指针更安全,不用担心 "悬空引用""野指针" 的问题。

1.4、值传递的适用场景

场景 1:传递基本数据类型

比如int、char、bool、double等,拷贝开销小,安全性优先。例如实现一个 "计算两数之和" 的函数:

复制代码
int add(int a, int b) { return a + b; }  // 值传递最适合

场景 2:传递小型结构体 / 对象

比如包含 2~3 个成员的结构体(如表示坐标的Point)、无复杂成员的类,拷贝开销可接受。例如:

复制代码
struct Point {
    int x;
    int y;
};
// 打印坐标,无需修改原始Point,用值传递
void printPoint(Point p) {
    cout << "(" << p.x << "," << p.y << ")" << endl;
}

场景 3:函数无需修改原始数据,且需保证数据安全

比如处理用户输入的验证函数、日志打印函数,用值传递可避免意外篡改原始数据。

Part2 引用传递

2.1、什么是引用传递?

引用(&)本质是变量的 "别名"------ 它没有独立的内存空间,而是和原始变量 "绑定" 在一起,共享同一块内存地址。引用传递的核心是 "传递别名关系",而非数据本身。

咱们先澄清一个常见误区:"引用是指针的语法糖"------ 从底层实现看,编译器确实会把引用当作 "隐式指针" 处理(比如在 64 位系统中,引用的底层也是 8 字节的地址),但语法上做了严格限制:

  • 引用必须在定义时立即绑定变量(不能像指针那样 "先定义,后赋值");
  • 引用一旦绑定,就不能再指向其他变量(指针可以随时改指向);
  • 引用不能绑定nullptr(指针可以指向空)。

这些限制让引用比指针更安全,同时又保留了 "直接操作原始数据" 的高效性。比如调用increment(num)时,形参int& x是num的别名 ------x的地址和num完全相同,操作x就等同于操作num。

示例:

复制代码
#include <iostream>
using namespace std;
// 形参x:num的别名,与num共享同一内存
void increment(int& x) {
    x++;  // 直接修改x(即num)的内存,地址与num一致
    cout << "函数内x的值:" << x << ",x的地址:" << &x << endl;  // 输出6,地址和num相同
}
int main() {
    int num = 5;  // num的地址假设为0x0012ff44
    cout << "函数外num初始值:" << num << ",num的地址:" << &num << endl;  // 输出5,地址0x0012ff44


    increment(num);  // 绑定x为num的别名,无数据拷贝


    cout << "函数外num最终值:" << num << endl;  // 输出6,原始数据被修改
    return 0;
}

2.2、引用传递的底层原理

  • 别名机制:引用本质是变量的别名(C++标准规定引用必须绑定到已存在的对象)。
  • 零开销:无需复制数据,直接通过内存地址访问。

2.3、常量引用与非常量引用

复制代码
// 常量引用:禁止修改实参
void print(const std::string &s) {
    std::cout << s << std::endl;
}
// 非常量引用:允许修改实参
void modify(std::string &s) {
    s += " modified";
}

常量引用的优势

  • 避免无意修改数据
  • 支持临时对象绑定(如print("hello"))

2.4、引用传递的 4 个核心特点

1)、零拷贝 overhead,效率拉满

引用传递不需要拷贝任何数据,只需要在编译期建立 "别名绑定"------ 哪怕传递的是 1GB 的大对象,开销也只是 "绑定地址"(底层指针的操作),比值传递的效率高几个数量级。这也是为什么传递std::string、std::vector这类大型容器时,几乎都用引用。

2)、const 引用是 "只读保护神"

如果咱们不需要修改原始数据,一定要用const T&(常量引用)------ 它有两个关键作用:

  • 禁止函数修改原始数据,保证安全性(比如void print(const vector<int>& vec),函数里不能改vec的元素);
  • 允许绑定临时变量(非 const 引用不行)。

比如:

复制代码
// 非const引用:不能传临时变量
void func1(string& s) {}
// const引用:可以传临时变量
void func2(const string& s) {}
int main() {
    // func1("hello");  // 编译报错:临时变量不能绑定到非const引用
    func2("hello");     // 编译通过:临时变量"hello"可绑定到const引用
    return 0;
}

这一点在实战中非常常用 ------ 比如函数参数是const string&,调用时既可以传变量,也可以传字符串字面量,灵活性更高。

3)、引用的生命周期必须 "小于等于" 原始变量

这是引用传递最容易踩的坑 ------ 如果引用绑定的变量被销毁了,引用就会变成 "悬空引用",再访问就会触发未定义行为(程序崩溃、乱码等)。比如:

复制代码
// 错误示例:返回局部变量的引用
int& getLocalRef() {
    int temp = 10;  // temp是局部变量,函数结束后销毁
    return temp;    // 返回temp的引用(悬空引用)
}
int main() {
    int& ref = getLocalRef();  // ref是悬空引用
    cout << ref << endl;       // 未定义行为:可能输出乱码或崩溃
    return 0;
}

解决办法:确保引用绑定的变量是 "长生命周期" 的(比如全局变量、堆上的变量、main 函数里的变量)。

4)、不会触发拷贝构造函数

因为引用传递不拷贝数据,所以自定义类对象传递时,不会调用拷贝构造函数 ------ 这也是比值传递高效的重要原因。比如:

复制代码
class MyClass {
public:
    MyClass() { cout << "默认构造" << endl; }
    MyClass(const MyClass& other) { cout << "拷贝构造" << endl; }
};
void func(MyClass& obj) {}  // 引用传递
int main() {
    MyClass a;  // 输出"默认构造"
    func(a);    // 无拷贝构造输出(直接绑定别名)
    return 0;
}

2.5、引用传递的适用场景

场景 1:传递大型对象 / 容器,且需避免拷贝

比如std::vector、std::map、自定义的大体积类(如Image、DataBuffer),用引用传递可节省大量拷贝时间。例如:

复制代码
// 处理大型vector,用const引用避免拷贝,且禁止修改
void processBigVector(const vector<int>& bigVec) {
    for (int val : bigVec) {
        // 只读操作
    }
}

场景 2:函数需要修改原始数据

比如实现 "排序函数""数据更新函数",用引用直接修改原始数据,无需通过返回值传递结果。例如:

复制代码
// 交换两个整数的值,用引用直接修改原始变量
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

场景 3:实现多返回值(比结构体更简洁)

C++ 函数只能有一个返回值,但用引用参数可以实现 "多返回值"------ 比如计算一个数组的 "总和" 和 "平均值":

复制代码
#include <vector>
using namespace std;
// 通过两个引用参数返回总和和平均值
void calculateSumAvg(const vector<int>& arr, int& sum, double& avg) {
    sum = 0;
    for (int val : arr) {
        sum += val;
    }
    avg = arr.empty() ? 0 : (double)sum / arr.size();
}
int main() {
    vector<int> arr = {1, 2, 3, 4, 5};
    int sum;
    double avg;
    calculateSumAvg(arr, sum, avg);
    cout << "总和:" << sum << ",平均值:" << avg << endl;  // 输出"总和:15,平均值:3"
    return 0;
}

Part3 指针传递

3.1、什么是指针传递?和引用有啥本质区别?

指针是存储变量内存地址 的变量 ------ 它有自己独立的内存空间(比如 64 位系统中占 8 字节),存储的是另一个变量的地址。指针传递的核心是 "传递指针的副本"------ 函数接收的形参是实参指针的拷贝,两者指向同一块内存,但形参本身是独立的(修改形参的指向不会影响实参)。

咱们先理清 "指针传递" 和 "引用传递" 的核心区别:

|-------|--------------|-------------|
| 对比维度 | 引用传递 | 指针传递 |
| 内存空间 | 无独立空间(别名) | 有独立空间(存地址) |
| 初始化要求 | 必须立即绑定变量 | 可先定义,后赋值 |
| 指向修改 | 一旦绑定,不能改指向 | 可随时修改指向 |
| 空值支持 | 不能指向 nullptr | 可指向 nullptr |

比如调用increment(&num)时,实参是num的地址(&num),形参int* x是这个地址的副本 ------x的内存里存的是num的地址,所以解引用*x就能操作num;但如果修改x本身(比如让x指向另一个变量temp),实参指针不会受影响,因为x只是副本。

示例

复制代码
#include <iostream>
using namespace std;
// 形参x:实参指针的副本,存储num的地址
void increment(int* x) {
    // 安全检查:避免空指针访问
    if (x == nullptr) {
        cout << "错误:指针为空!" << endl;
        return;
    }


    (*x)++;  // 解引用:通过地址访问num的内存,修改原始数据
    cout << "函数内*x的值:" << *x << endl;  // 输出6
    cout << "函数内x的地址(指针本身的地址):" << &x << endl;  // x是副本,地址独立
}
int main() {
    int num = 5;
    int* ptr = &num;  // ptr存储num的地址(比如0x0012ff44)


    cout << "函数外ptr存储的地址:" << ptr << endl;  // 输出0x0012ff44
    cout << "函数外ptr本身的地址:" << &ptr << endl;  // 比如0x0012ff40(实参指针地址)


    increment(ptr);  // 传递ptr的副本(存储0x0012ff44)


    cout << "函数外num的值:" << num << endl;  // 输出6,原始数据被修改
    return 0;
}

3.2、指针传递的 4 个关键特点

1)、灵活性最高,但安全性最低

指针的 "可修改指向" 是最大优势 ------ 比如遍历链表时,指针可以从 "当前节点" 指向 "下一个节点";但这也是风险点:如果操作不当,很容易出现空指针 (指向nullptr)或野指针(指向已销毁的内存)。

举个野指针的典型场景:

复制代码
// 错误示例:返回局部变量的指针
int* getLocalPtr() {
    int temp = 10;  // temp是局部变量,函数结束后栈帧销毁
    return &temp;   // 返回temp的地址(野指针)
}
int main() {
    int* p = getLocalPtr();  // p是野指针,指向已销毁的内存
    cout << *p << endl;      // 未定义行为:可能输出乱码、崩溃
    return 0;
}

避坑办法:

  • 指针使用前必须检查nullptr(用if (p != nullptr));
  • 避免返回局部变量的指针;
  • 动态内存分配的指针(new出来的),用完后必须delete,避免内存泄漏。

2)、传递数组的 "唯一方式"

C++ 中数组名本质是 "指向首元素的指针",传递数组时,实际上是传递数组首元素的指针(即指针传递)。比如:

复制代码
// 传递数组:arr是指向首元素的指针,len是数组长度(必须显式传递,数组名不含长度信息)
void printArray(int* arr, int len) {
    for (int i = 0; i < len; i++) {
        cout << arr[i] << " ";  // arr[i]等价于*(arr + i)
    }
}
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int len = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度
    printArray(arr, len);  // 传递数组首元素指针(arr)和长度
    return 0;
}

这里要注意:数组传递时不会拷贝整个数组,只会传递首元素地址,所以必须显式传递数组长度(否则函数不知道数组有多少元素)。

3)、二级指针:修改一级指针的指向

前面说过 "指针传递时,修改形参指针的指向不会影响实参"------ 那如果咱们想在函数里修改一级指针的指向(比如动态分配内存),该怎么办?答案是二级指针(指针的指针,T**)。

比如在函数里为一级指针分配动态内存:

复制代码
#include <iostream>
using namespace std;
// 二级指针:ptr是一级指针arr的地址,*ptr就是arr本身
void allocateMemory(int** ptr, int size) {
    if (ptr == nullptr) return;
    // 为一级指针arr分配内存(*ptr = arr)
    *ptr = new int[size];
    // 初始化数组
    for (int i = 0; i < size; i++) {
        (*ptr)[i] = i;  // (*ptr)[i]等价于arr[i]
    }
}
int main() {
    int* arr = nullptr;  // 一级指针,初始为空
    int size = 5;


    allocateMemory(&arr, size);  // 传递一级指针的地址(二级指针)


    // 使用数组
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";  // 输出0 1 2 3 4
    }


    // 释放内存(避免泄漏)
    delete[] arr;
    arr = nullptr;  // 避免野指针
    return 0;
}

这里的关键逻辑:&arr是一级指针arr的地址(二级指针),函数里*ptr就是arr本身,所以*ptr = new int[size]相当于直接修改arr的指向,让它指向新分配的堆内存。

4)、兼容 C 语言代码

C 语言没有 "引用" 特性,所有需要 "操作原始数据" 的场景都用指针 ------ 如果咱们的 C++ 项目需要调用 C 库函数(比如stdio.h、string.h里的函数),就必须用指针传递。比如调用 C 的strcpy函数:

复制代码
#include <cstring>  // C库字符串函数
using namespace std;
int main() {
    char dest[20];
    const char* src = "hello world";
    strcpy(dest, src);  // C函数,参数是指针
    cout << dest << endl;  // 输出hello world
    return 0;
}

3.3、指针传递的适用场景

场景 1:处理可选参数(允许传递 nullptr)

如果函数的某个参数是 "可选的"(比如 "可选的输出参数"),用指针传递最合适 ------ 不需要该参数时,传递nullptr即可。例如:

复制代码
#include <iostream>
using namespace std;
// 计算x的平方,result是可选输出参数(不需要则传nullptr)
void square(int x, int* result = nullptr) {
    int res = x * x;
    if (result != nullptr) {
        *result = res;  // 需要输出时,赋值给result
    }
    cout << x << "的平方是:" << res << endl;  // 必打印结果
}
int main() {
    int res;
    square(5, &res);  // 需要输出,传递res的地址
    cout << "存储的结果:" << res << endl;  // 输出25


    square(6);         // 不需要输出,传递nullptr(默认值)
    return 0;
}

场景 2:修改一级指针的指向(必须用二级指针)

比如动态内存分配、链表节点插入 / 删除(修改头指针指向)、树的节点操作等场景,只能用二级指针或 "指针的引用"(T*&)。

场景 3:传递数组或动态内存(堆上的对象)

数组传递只能用指针(配合长度参数);堆上的对象(new出来的)也必须用指针访问,传递时自然是指针传递。

场景 4:兼容 C 语言代码或旧版 C++ 代码

如果项目需要和 C 代码交互,或者维护旧的 C++ 代码(未使用引用特性),必须用指针传递。

Part4 三种传递机制对比

咱们从 "底层实现""性能""安全性""实战场景" 等 7 个维度,做一个全方位对比,帮大家快速决策:

|----------|--------------------|-------------------------|-----------------------|
| 对比维度 | 值传递(Pass-by-Value) | 引用传递(Pass-by-Reference) | 指针传递(Pass-by-Pointer) |
| 传递的内容 | 实参的完整数据副本 | 实参的别名(绑定地址) | 实参指针的副本(存地址) |
| 底层内存开销 | 高(拷贝全部数据,大对象明显) | 低(仅绑定地址,无数据拷贝) | 低(仅拷贝地址,4/8 字节) |
| 能否修改原始数据 | 不能(仅改副本) | 能(const可禁止) | 能(const可禁止,需解引用) |
| 空值支持 | 不涉及(传递的是数据) | 不支持(不能绑定 nullptr) | 支持(可传 nullptr,需检查) |
| 生命周期依赖 | 无(形参独立) | 有(引用 ≤ 实参生命周期) | 有(指针指向的内存需有效) |
| 语法复杂度 | 简单(直接传值) | 简单(&声明,直接访问) | 较复杂(*解引用、&取地址) |
| 拷贝构造调用 | 会(对象传递时) | 不会(无数据拷贝) | 不会(无数据拷贝) |
| 核心适用场景 | 小数据、只读操作、安全优先 | 大数据、需修改、非空参数 | 可选参数、二级指针、兼容 C 代码 |
| 常见错误 | 传递大对象导致性能差 | 绑定临时变量(非 const)、悬空引用 | 空指针未检查、野指针、内存泄漏 |

Part5 实战决策流程

5.1、三步搞定参数传递选择(不用再纠结)

咱们在实际开发中,不用死记硬背,按这个流程选就行:

1)、第一步:判断是否需要修改原始数据?

  • 不允许为空(参数必须有效)→ 用引用传递(安全,无空指针风险);
  • 允许为空(可选参数)→ 用指针传递(需加 nullptr 检查)。
  • 数据小(基本类型、小型结构体)→ 用值传递(安全简单);
  • 数据大(大型对象、容器)→ 用const 引用(高效,禁止修改);
  • 不需要修改 → 看数据大小:
  • 需要修改 → 看是否允许参数为空:

2)、第二步:判断是否需要兼容 C 代码?

  • 是 → 必须用指针传递
  • 否 → 优先用引用(语法简洁,安全性高)。

3)、第三步:判断是否需要修改指针指向?

  • 是(如动态内存分配、修改链表头指针)→ 用二级指针指针的引用
  • 否 → 按第一步、第二步选择。

5.2、最容易踩的 5 个坑(避坑指南)

坑 1:用值传递传递大型对象

比如传递vector<int> bigVec(1000000, 1),值传递会拷贝 100 万个int,直接导致性能崩溃。

避坑:用const vector<int>&。

坑 2:引用绑定临时变量(非 const)

比如void func(string& s) {},调用func("hello")会编译报错。

避坑:用const string&,允许绑定临时变量。

坑 3:返回局部变量的引用 / 指针

比如前面的getLocalRef()和getLocalPtr(),返回后变量已销毁,形成悬空引用 / 野指针。

避坑:返回全局变量、堆上的变量,或直接返回值(小数据)。

坑 4:指针未检查 nullptr

比如void func(int* x) { (*x)++; },调用func(nullptr)会崩溃。

避坑:所有指针使用前,必须加if (x != nullptr)检查。

坑 5:动态内存分配后不释放

比如int* p = new int[5];,用完后不delete[] p,导致内存泄漏。

避坑:用智能指针(unique_ptr、shared_ptr)管理动态内存,或严格遵循 "谁分配谁释放"。

总结

C++ 的三种参数传递机制,本质是 "效率 " 和 "安全性" 的权衡:

  • 值传递是 "安全派",适合小数据、只读场景,但拷贝开销大;
  • 引用传递是 "平衡派",兼顾高效和安全,是大多数 C++ 场景的首选;
  • 指针传递是 "灵活派",适合可选参数、兼容 C 代码,但需手动规避空指针 / 野指针风险。

咱们在写代码时,不用追求 "某一种方式万能",而是根据具体场景选择 ------ 比如传递int用值传递,传递vector用 const 引用,传递可选参数用指针。只有理解每种机制的底层逻辑和适用边界,才能写出高效、安全、易维护的 C++ 代码。

往期文章推荐

【大厂标准】Linux C/C++ 后端进阶学习路线

解构内存池:C++高性能编程的底层密码

知识点精讲:深入理解C/C++指针

总被 "算法" 难住?程序员怎样学好算法?

小米C++校招二面:epoll和poll还有select区别,底层方式?

顺时针螺旋移动法 | 彻底弄懂复杂C/C++嵌套声明、const常量声明!!!

C++ 基于原子操作实现高并发跳表结构

为什么很多人劝退学 C++,但大厂核心岗位还是要 C++?

手撕线程池:C++程序员的能力试金石

相关推荐
liu****4 小时前
8.list的使用
数据结构·c++·算法·list
立志成为大牛的小牛4 小时前
数据结构——二十六、邻接表(王道408)
开发语言·数据结构·c++·学习·程序人生
草莓熊Lotso4 小时前
C++ 方向 Web 自动化测试入门指南:从概念到 Selenium 实战
前端·c++·python·selenium
CoderCodingNo5 小时前
【GESP】C++五级考试大纲知识点梳理, (5) 算法复杂度估算(多项式、对数)
开发语言·c++·算法
星河队长5 小时前
VS创建C++动态库和C#访问过程
java·c++·c#
沐怡旸7 小时前
【穿越Effective C++】条款02:尽量以const, enum, inline替换#define
c++·面试
给大佬递杯卡布奇诺7 小时前
FFmpeg 基本API avcodec_alloc_context3函数内部调用流程分析
c++·ffmpeg·音视频
QT 小鲜肉7 小时前
【个人成长笔记】Qt 中 SkipEmptyParts 编译错误解决方案及版本兼容性指南
数据库·c++·笔记·qt·学习·学习方法
看到我,请让我去学习8 小时前
Qt 控件 QSS 样式大全(通用属性篇)
开发语言·c++·qt