0

0

C++结构体与函数参数传递方法

P粉602998670

P粉602998670

发布时间:2025-09-21 13:43:01

|

700人浏览过

|

来源于php中文网

原创

结构体作为函数参数传递有值传递、引用传递和指针传递三种方式。值传递安全但开销大,适用于小型结构体;引用传递高效且能修改原数据,常用于大型结构体或需修改的场景;指针传递灵活支持可选参数和C语言兼容,但需注意空指针检查。对于只读操作,const引用是最佳实践,避免复制并保证安全性。多线程下需防范数据竞争,通过互斥锁等同步机制保护共享结构体。

c++结构体与函数参数传递方法

C++中结构体作为函数参数的传递,主要有三种方式:值传递、引用传递和指针传递。每种方式都有其适用场景和性能考量,理解它们的核心差异对于写出高效、健壮的代码至关重要。简单来说,如果你想在函数内部修改结构体,并让这些修改影响到函数外部的原始结构体,那么引用或指针传递是你的选择;如果只是想读取结构体数据,不希望修改它,同时又想避免不必要的拷贝开销,

const
引用通常是最佳实践;而对于小型结构体,值传递则显得直接且安全。

解决方案

在C++中,将结构体作为函数参数传递,我们通常有以下几种实践方式,每种都有其独特之处和适用场景。我个人在日常开发中,会根据具体需求和结构体的大小来灵活选择。

1. 值传递 (Pass by Value)

这是最直观的方式,函数会接收到结构体的一个完整副本。

立即学习C++免费学习笔记(深入)”;

struct MyStruct {
    int id;
    std::string name;
    // 假设还有很多其他成员...
};

void processStructByValue(MyStruct s) {
    s.id = 100; // 修改的是副本
    std::cout << "Inside func (by value): id = " << s.id << ", name = " << s.name << std::endl;
}

// 调用示例:
// MyStruct data = {1, "Original"};
// processStructByValue(data);
// std::cout << "Outside func: id = " << data.id << std::endl; // data.id 仍然是 1

这种方式的好处是安全,函数内部对结构体的任何修改都不会影响到外部的原始数据,因为它操作的是一个独立的副本。但缺点也很明显:如果结构体包含大量数据或者复杂的成员(比如其他对象),复制整个结构体会带来显著的性能开销,包括内存分配和复制构造函数调用。对于大型结构体,这几乎是应该避免的。

2. 引用传递 (Pass by Reference)

引用传递允许函数直接操作原始结构体,而不是它的副本。这是C++中非常常用且高效的方式。

struct MyStruct {
    int id;
    std::string name;
};

void modifyStructByReference(MyStruct& s) {
    s.id = 200; // 直接修改原始结构体
    s.name = "Modified";
    std::cout << "Inside func (by ref): id = " << s.id << ", name = " << s.name << std::endl;
}

void printStructByConstReference(const MyStruct& s) {
    // s.id = 300; // 编译错误:不能修改const引用
    std::cout << "Inside func (by const ref): id = " << s.id << ", name = " << s.name << std::endl;
}

// 调用示例:
// MyStruct data = {1, "Original"};
// modifyStructByReference(data);
// std::cout << "Outside func: id = " << data.id << ", name = " << data.name << std::endl; // data.id 是 200, name 是 "Modified"
// printStructByConstReference(data); // 安全地打印

引用传递的优点是效率高,避免了不必要的复制。如果函数需要修改结构体,这是首选。如果函数只是需要读取结构体数据而不修改它,使用

const MyStruct&
(常量引用)是最佳实践。它既避免了复制开销,又通过
const
关键字保证了数据不被意外修改,提高了代码的健壮性。

3. 指针传递 (Pass by Pointer)

指针传递与引用传递在某些方面相似,都是传递地址而非副本,从而避免了复制开销并允许修改原始数据。

struct MyStruct {
    int id;
    std::string name;
};

void modifyStructByPointer(MyStruct* sPtr) {
    if (sPtr) { // 良好的编程习惯:检查指针是否为空
        sPtr->id = 300; // 通过指针修改原始结构体
        sPtr->name = "PointerModified";
        std::cout << "Inside func (by ptr): id = " << sPtr->id << ", name = " << sPtr->name << std::endl;
    }
}

void printStructByConstPointer(const MyStruct* sPtr) {
    if (sPtr) {
        // sPtr->id = 400; // 编译错误:不能修改const指针指向的数据
        std::cout << "Inside func (by const ptr): id = " << sPtr->id << ", name = " << sPtr->name << std::endl;
    }
}

// 调用示例:
// MyStruct data = {1, "Original"};
// modifyStructByPointer(&data);
// std::cout << "Outside func: id = " << data.id << ", name = " << data.name << std::endl; // data.id 是 300, name 是 "PointerModified"
// printStructByConstPointer(&data);

指针传递的优点同样是效率高,且允许修改原始数据。与引用相比,指针更加灵活,可以指向

nullptr
,这使得在某些场景下(例如可选参数)指针更具表达力。然而,这也带来了额外的责任:调用者必须确保传递一个有效的指针,并且函数内部通常需要进行空指针检查,这增加了代码的复杂性。对于只读访问,也可以使用
const MyStruct*

什么时候应该选择值传递,什么时候选择引用或指针传递?

这确实是一个开发者经常纠结的问题,我个人在做选择时,通常会从以下几个角度去考量:

首先,结构体的大小是决定性因素。如果你的结构体非常小,比如只包含几个

int
char
,甚至只有一两个指针,那么值传递的开销可能微乎其微,甚至比引用传递更优。这是因为现代CPU对小块数据的复制非常高效,而且引用或指针传递本身也有很小的寻址开销。我通常把“小”定义为小于或等于一个机器字长(例如8字节或16字节)的简单类型。对于这类结构体,值传递能带来更好的局部性,代码也更直接,无需担心空指针或引用生命周期的问题。

然而,一旦结构体稍微大一点,或者它内部包含

std::string
std::vector
或其他自定义对象,值传递的性能劣势就会迅速显现。每次函数调用都会触发深拷贝(如果成员有自定义拷贝构造函数),这不仅消耗CPU时间,还可能导致内存频繁分配和释放,对性能和内存碎片化都有影响。

其次,函数是否需要修改原始数据。这是另一个核心判断点。

  • 如果函数需要修改结构体,并且这些修改需要反映到函数调用者那里,那么引用传递(
    MyStruct&
    )或指针传递(
    MyStruct*
    )是唯一的选择。在C++中,我个人更倾向于使用引用,因为它避免了指针的空值检查和解引用语法(
    ->
    vs.
    .
    ),代码看起来更简洁、更安全。
  • 如果函数不需要修改结构体,仅仅是读取数据,那么
    const
    引用传递(
    const MyStruct&
    几乎总是最佳实践。它完美地结合了效率(避免复制)和安全性(编译器保证不修改)。这在我看来是C++中处理结构体参数的“黄金法则”。它避免了大型结构体的复制开销,同时通过
    const
    确保了数据的不可变性,这对于提高代码的可读性和维护性非常有帮助。

指针传递则在以下场景中更具优势:

  • 可选参数:当一个参数可能是可选的,即它可能存在也可能不存在时,传递一个指针允许你传递
    nullptr
    来表示“不存在”。引用则必须绑定到一个实际存在的对象。
  • 需要动态分配的内存:如果你在函数内部需要分配一个新的结构体实例,并将其所有权传递给调用者,那么返回一个指针或通过指针参数来修改外部指针变量会是常见的做法。
  • 与C语言API交互:许多C语言的库函数都使用指针作为参数,为了兼容性,C++代码有时也会采用指针。

总结来说,我的建议是:

  • 小结构体:值传递。
  • 大结构体,需要修改:引用传递 (
    MyStruct&
    )。
  • 大结构体,只读
    const
    引用传递 (
    const MyStruct&
    )。
  • 需要可选参数或与C API交互:指针传递 (
    MyStruct*
    const MyStruct*
    )。

值传递时,大型结构体对程序性能有何影响?有什么优化手段吗?

当大型结构体以值传递的方式作为函数参数时,对程序性能的影响是相当显著且多方面的,这绝不仅仅是“慢一点”那么简单。在我看来,这通常是一个需要警惕的性能陷阱。

性能影响分析:

  1. 高昂的复制开销:这是最直接的影响。当一个大型结构体被值传递时,编译器会生成代码来调用其拷贝构造函数(如果用户定义了,或者编译器生成默认的逐成员拷贝)。这意味着结构体内部的所有成员都会被复制一份。如果结构体内部包含

    std::string
    std::vector
    或其他动态分配内存的成员,那么拷贝操作可能涉及深层复制,这会触发大量的内存分配(
    new
    /
    malloc
    )和数据复制,从而消耗大量的CPU周期和内存带宽。想象一下,一个包含几百KB数据的结构体,每次函数调用都要复制一遍,这效率可想而知。

  2. 增加内存使用:每次值传递都会在函数的帧上创建一个新的结构体副本。如果函数被频繁调用,或者在递归调用中,这会导致栈空间快速增长,甚至可能导致栈溢出。即使不是栈溢出,额外的内存占用也可能导致缓存效率下降。

    杰易OA办公自动化系统6.0
    杰易OA办公自动化系统6.0

    基于Intranet/Internet 的Web下的办公自动化系统,采用了当今最先进的PHP技术,是综合大量用户的需求,经过充分的用户论证的基础上开发出来的,独特的即时信息、短信、电子邮件系统、完善的工作流、数据库安全备份等功能使得信息在企业内部传递效率极大提高,信息传递过程中耗费降到最低。办公人员得以从繁杂的日常办公事务处理中解放出来,参与更多的富于思考性和创造性的工作。系统力求突出体系结构简明

    下载
  3. 缓存失效:CPU缓存是现代处理器性能的关键。当大量数据被复制时,新的数据副本可能会冲刷掉CPU缓存中原有的一些有用数据,导致后续访问时发生缓存未命中,不得不从较慢的主内存中重新加载数据,这会严重拖慢程序执行速度。

  4. 不必要的构造/析构函数调用:每次创建副本都会调用拷贝构造函数,函数结束时副本被销毁则会调用析构函数。对于复杂结构体,这些构造和析构操作本身就可能包含复杂的逻辑和资源管理,进一步加剧性能负担。

优化手段:

对于大型结构体,最主要的“优化手段”其实是避免值传递,转而采用更高效的传递方式。

  1. 使用

    const
    引用传递 (Pass by
    const
    Reference)
    :这是最常用且最有效的优化。如果函数不需要修改结构体内容,将其声明为
    const MyStruct&
    。这既避免了复制开销,又通过
    const
    关键字保证了数据安全。

    struct LargeData {
        std::vector data; // 假设数据量很大
        // ... 其他成员
    };
    
    void processLargeDataEfficiently(const LargeData& d) {
        // 只能读取 d 的内容,不能修改
        for (int val : d.data) {
            // ...
        }
    }
  2. 使用引用传递 (Pass by Reference):如果函数确实需要修改结构体内容,那么使用

    MyStruct&
    是必要的。它同样避免了复制。

    void modifyLargeData(LargeData& d) {
        d.data.push_back(100); // 修改 d 的内容
    }
  3. 使用移动语义 (Move Semantics, C++11及更高版本):这是一个更高级的优化,适用于当你希望将结构体的“所有权”从调用者转移到被调用函数,并且调用者不再需要原始结构体时。通过右值引用(

    MyStruct&&
    ),可以避免深拷贝,而是将资源(如动态分配的内存指针)从源对象“窃取”到目标对象,从而实现零开销的转移。

    void consumeLargeData(LargeData&& d) {
        // d 现在拥有了原数据的资源,原数据可能处于有效但未指定状态
        // 可以在这里对 d 进行修改或进一步处理
        d.data.clear(); // 比如,清空数据
    }
    
    // 调用示例:
    // LargeData original_data;
    // original_data.data.resize(1000000);
    // consumeLargeData(std::move(original_data)); // 显式地将所有权转移
    // // original_data 在此之后不应再被使用,因为它已被“移动”

    移动语义对于那些需要转移资源(如

    std::vector
    std::string
    、智能指针等)的结构体尤其有效,它将拷贝的O(N)操作变成了O(1)的指针交换操作。

  4. 返回局部结构体优化 (Return Value Optimization, RVO/NRVO):虽然这主要针对函数返回值,但与参数传递的性能考量有共通之处。现代编译器通常能够优化掉函数返回局部对象时的拷贝操作。当函数返回一个大型结构体时,如果编译器能够进行RVO,那么性能影响会大大降低。但请注意,RVO是编译器的一种优化,我们不能完全依赖它,且它与函数参数传递是不同的场景。

在我看来,最根本的优化思想是:尽量避免不必要的数据复制。对于大型结构体,只要不是必须拥有一个独立副本的场景,都应该优先考虑引用或

const
引用。移动语义则是在需要转移所有权时的强大工具

在多线程环境下,结构体参数传递需要注意哪些并发问题?

在多线程环境下,结构体作为函数参数的传递方式,直接关系到数据共享和并发安全。如果不加注意,很容易引入数据竞争(data race),导致程序行为不可预测甚至崩溃。

  1. 值传递 (Pass by Value) 的并发安全性

    当结构体以值传递方式传入多线程函数时,每个线程都会获得结构体的一个独立副本。这意味着,即使多个线程同时调用这个函数,它们操作的也是各自栈上的数据副本,彼此之间不会相互影响。从这个角度看,值传递在函数内部是并发安全的,因为它天然地隔离了数据。

    struct ThreadSafeData {
        int value;
        // ...
    };
    
    void processInThread(ThreadSafeData data_copy) {
        // data_copy 是线程私有的副本,修改它不会影响其他线程
        data_copy.value++;
        std::cout << "Thread " << std::this_thread::get_id() << ": " << data_copy.value << std::endl;
    }
    
    // 调用示例:
    // ThreadSafeData shared_original = {0};
    // std::thread t1(processInThread, shared_original);
    // std::thread t2(processInThread, shared_original);
    // t1.join(); t2.join();
    // // shared_original.value 仍然是 0

    然而,这并不意味着就没有并发问题了。如果这个结构体本身在创建时就包含了指向共享资源的指针或引用(例如,一个指向全局

    std::vector
    的指针),那么即使是副本,其内部的指针仍然可能指向同一个共享资源。此时,对共享资源的访问仍然需要同步。

  2. 引用传递 (Pass by Reference) 和指针传递 (Pass by Pointer) 的并发风险

    这两种方式都允许函数直接访问和修改原始结构体。在多线程环境中,如果多个线程通过引用或指针同时访问(至少一个访问是写入)同一个结构体实例,那么就会发生数据竞争。这是非常危险的,可能导致:

    • 不确定的结果:线程执行顺序不确定,导致最终结果不符合预期。
    • 程序崩溃:写入操作可能破坏数据结构,导致野指针、内存访问越界等问题。
    • 死锁:如果同步机制使用不当,可能导致线程相互等待而无法继续执行。
    struct SharedMutableData {
        int counter;
        std::mutex mtx; // 用于保护 counter
    };
    
    void modifyInThread(SharedMutableData& data_ref) {
        // 错误示例:没有加锁直接修改,可能导致数据竞争
        // data_ref.counter++;
    
        // 正确做法:使用互斥锁保护共享数据
        std::lock_guard lock(data_ref.mtx);
        data_ref.counter++;
        std::cout << "Thread " << std::this_thread::get_id() << ": " << data_ref.counter << std::endl;
    }
    
    // 调用示例:
    // SharedMutableData shared_data = {0};
    // std::thread t1(modifyInThread, std::ref(shared_data)); // 注意 std::ref
    // std::thread t2(modifyInThread, std::ref(shared_data));
    // t1.join(); t2.join();
    // // shared_data.counter 最终会是 2 (如果正确加锁)

    需要注意的并发问题和解决方案:

    • 数据竞争 (Data Race):这是最核心的问题。当至少两个线程并发访问同一个内存位置,并且至少一个访问是写入操作时,且没有进行适当的同步,就会发生数据竞争。

      • 解决方案:使用互斥锁(
        std::mutex
        )、读写锁(
        std::shared_mutex
        )、原子操作(
        std::atomic
        )或条件变量(
        std::condition_variable
        )等同步机制来保护共享结构体。确保在访问或修改共享结构体之前获取锁,操作完成后释放锁。
    • 死锁 (Deadlock):如果多个线程需要获取多个锁,并且获取顺序不一致,就可能发生死锁。

      • 解决方案:统一锁的获取顺序;使用
        std::lock()
        一次性获取多个锁;避免在持有锁的情况下调用可能阻塞的函数。
    • 可见性问题 (Visibility Issues):一个线程对共享数据的修改,不一定能立即被另一个线程看到。

      • 解决方案:使用
        std::atomic
        类型可以保证操作的原子性和内存顺序(即可见性);互斥锁也会隐式地提供内存同步,确保锁释放前的数据修改对获取锁后的线程可见。
    • const
      引用与
      mutable
      成员
      :即使你传递的是
      const MyStruct&
      ,如果结构体内部有
      mutable
      成员,或者通过
      const_cast
      强制转换掉了
      const
      属性,那么仍然可能在多线程环境中修改共享数据,从而引入数据竞争。

      • 解决方案:避免在多线程环境下对
        const
        对象进行
        const_cast
        。如果
        mutable
        成员确实需要在
        const
        函数中修改,并且是共享的,那么它也必须被适当

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

384

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

609

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

351

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

256

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

593

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

520

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

636

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

599

2023.09.22

c++主流开发框架汇总
c++主流开发框架汇总

本专题整合了c++开发框架推荐,阅读专题下面的文章了解更多详细内容。

25

2026.01.09

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Swoft2.x速学之http api篇课程
Swoft2.x速学之http api篇课程

共16课时 | 0.9万人学习

PHP基础入门课程
PHP基础入门课程

共33课时 | 1.9万人学习

Golang进阶实战编程
Golang进阶实战编程

共34课时 | 2.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号