MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 函数的参数传递

2021-11-172.1k 阅读

C++ 函数参数传递基础

在 C++ 编程中,函数是模块化编程的核心组件,而函数的参数传递机制决定了数据如何从调用函数传递到被调用函数。正确理解和运用函数参数传递方式,对于编写高效、健壮且易于维护的代码至关重要。

值传递

值传递是 C++ 中最基本的参数传递方式。当使用值传递时,实参的值被复制到形参中,在被调用函数内部对形参的任何修改都不会影响到调用函数中的实参。

下面通过一个简单的示例来展示值传递:

#include <iostream>

void increment(int num) {
    num++;
    std::cout << "In increment function, num is: " << num << std::endl;
}

int main() {
    int value = 10;
    std::cout << "Before calling increment, value is: " << value << std::endl;
    increment(value);
    std::cout << "After calling increment, value is: " << value << std::endl;
    return 0;
}

在上述代码中,increment 函数接受一个 int 类型的参数 num。在 main 函数中,定义了一个变量 value 并初始化为 10,然后将 value 作为实参传递给 increment 函数。在 increment 函数内部,对 num 进行自增操作,这只会改变 num 的值,而不会影响 main 函数中的 value。从输出结果可以清晰地看到,value 的值在调用 increment 函数前后并未发生改变。

值传递的优点在于简单直观,对于简单数据类型(如 intfloat 等),复制操作效率较高。然而,当传递大型对象时,值传递可能会导致性能问题,因为需要复制整个对象,这会消耗较多的时间和内存。

指针传递

指针传递允许我们将实参的地址传递给被调用函数,这样在被调用函数中可以通过指针来访问和修改调用函数中的实参。

以下是一个使用指针传递的示例:

#include <iostream>

void increment(int* numPtr) {
    (*numPtr)++;
    std::cout << "In increment function, num is: " << *numPtr << std::endl;
}

int main() {
    int value = 10;
    std::cout << "Before calling increment, value is: " << value << std::endl;
    increment(&value);
    std::cout << "After calling increment, value is: " << value << std::endl;
    return 0;
}

在这个例子中,increment 函数接受一个 int 类型的指针 numPtr。在 main 函数中,通过取地址符 & 获取 value 的地址,并将其作为实参传递给 increment 函数。在 increment 函数内部,通过解引用指针 *numPtr 来访问和修改 main 函数中的 value。从输出结果可以看出,value 的值在调用 increment 函数后发生了改变。

指针传递的优点是可以在被调用函数中修改调用函数的实参,同时对于大型对象,传递指针比传递整个对象要高效得多,因为只需要复制一个指针(通常是 4 字节或 8 字节,取决于系统架构),而不是整个对象。然而,指针传递也增加了代码的复杂性,因为需要手动管理指针,如指针的初始化、解引用和内存释放等,不正确的操作可能会导致内存泄漏或悬空指针等问题。

引用传递

引用传递是 C++ 中一种更安全、更简洁的传递参数方式,它本质上也是传递实参的地址,但语法上看起来像是直接传递变量。

下面是一个引用传递的示例:

#include <iostream>

void increment(int& num) {
    num++;
    std::cout << "In increment function, num is: " << num << std::endl;
}

int main() {
    int value = 10;
    std::cout << "Before calling increment, value is: " << value << std::endl;
    increment(value);
    std::cout << "After calling increment, value is: " << value << std::endl;
    return 0;
}

在这个代码中,increment 函数接受一个 int 类型的引用 num。在 main 函数中,直接将 value 作为实参传递给 increment 函数。在 increment 函数内部对 num 的修改,实际上就是对 main 函数中 value 的修改。从输出结果可以看到,value 的值在调用 increment 函数后发生了变化。

引用传递结合了指针传递能修改实参的优点,同时在语法上更加简洁直观,避免了指针传递中繁琐的指针操作,减少了因指针使用不当而引发错误的可能性。此外,对于大型对象,引用传递同样只传递对象的地址,具有较高的效率。

不同数据类型的参数传递

基本数据类型的参数传递

基本数据类型(如 intfloatchar 等)在进行值传递时,由于其占用内存空间较小,复制操作通常效率较高。例如,int 类型通常占用 4 字节(在 32 位系统下)或 8 字节(在 64 位系统下),值传递时复制这样小的一块内存几乎不会带来性能问题。

#include <iostream>

void printDouble(float num) {
    std::cout << "Double of num is: " << num * 2 << std::endl;
}

int main() {
    float value = 5.5f;
    printDouble(value);
    return 0;
}

在上述代码中,printDouble 函数接受一个 float 类型的参数 num,采用值传递方式。main 函数中的 value 被复制到 num 中,函数内部对 num 的操作不会影响 value

而对于指针传递和引用传递基本数据类型,虽然也能实现修改实参的目的,但在实际应用中,对于基本数据类型如果不需要修改实参,值传递通常是更简单直接的选择。只有在明确需要修改调用函数中的实参时,才考虑指针传递或引用传递。

数组的参数传递

在 C++ 中,数组作为函数参数传递时,情况较为特殊。当我们将数组作为参数传递给函数时,实际上传递的是数组的首地址,这类似于指针传递。

#include <iostream>

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    printArray(numbers, 5);
    return 0;
}

在这个例子中,printArray 函数接受一个 int 类型的数组 arr 和数组的大小 size。在 main 函数中,定义了数组 numbers 并传递给 printArray 函数。由于传递的是数组首地址,在 printArray 函数中可以访问和修改数组中的元素,这些修改会反映到 main 函数中的 numbers 数组上。

需要注意的是,在函数声明中,数组参数可以写成 int arr[]int* arr,这两种形式在函数参数传递时效果是一样的,都表示传递的是数组首地址。

另外,C++ 标准库提供了 std::vector 来替代传统数组,std::vector 作为函数参数传递时,可以采用值传递、指针传递(通过 & 获取 std::vector 的地址)或引用传递的方式。值传递 std::vector 会复制整个 std::vector 对象,包括其内部的所有元素,这在 std::vector 较大时性能较低;而指针传递和引用传递则更高效,同时也能方便地修改 std::vector 的内容。

结构体和类对象的参数传递

  1. 值传递结构体和类对象 当使用值传递方式传递结构体或类对象时,会复制整个对象。这意味着对象的所有成员变量都会被复制,对于包含大量成员变量或复杂成员变量(如动态分配内存的成员变量)的对象,值传递可能会带来较大的性能开销。
#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

void printPerson(Person p) {
    std::cout << "Name: " << p.name << ", Age: " << p.age << std::endl;
}

int main() {
    Person person1 = {"Alice", 30};
    printPerson(person1);
    return 0;
}

在上述代码中,Person 结构体包含一个 std::string 类型的 name 和一个 int 类型的 ageprintPerson 函数采用值传递方式接受一个 Person 对象 p。在 main 函数中,创建了 person1 并传递给 printPerson 函数,此时会复制 person1p

  1. 指针传递结构体和类对象 通过指针传递结构体或类对象,可以避免复制整个对象,提高效率。同时,通过指针可以在被调用函数中修改对象的成员变量。
#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

void incrementAge(Person* p) {
    p->age++;
    std::cout << "Incremented age of " << p->name << " to: " << p->age << std::endl;
}

int main() {
    Person person1 = {"Bob", 25};
    incrementAge(&person1);
    std::cout << "Age of " << person1.name << " after increment is: " << person1.age << std::endl;
    return 0;
}

在这个例子中,incrementAge 函数接受一个 Person 结构体指针 p。在 main 函数中,通过取地址符 &person1 的地址传递给 incrementAge 函数,函数内部通过指针修改 person1age 成员变量。

  1. 引用传递结构体和类对象 引用传递结构体或类对象结合了指针传递的高效性和值传递的简洁语法,是传递结构体和类对象的常用方式。
#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

void updatePerson(Person& p, const std::string& newName, int newAge) {
    p.name = newName;
    p.age = newAge;
    std::cout << "Updated person: Name: " << p.name << ", Age: " << p.age << std::endl;
}

int main() {
    Person person1 = {"Charlie", 28};
    updatePerson(person1, "David", 32);
    std::cout << "Final person: Name: " << person1.name << ", Age: " << person1.age << std::endl;
    return 0;
}

在这个代码中,updatePerson 函数接受一个 Person 结构体引用 p 以及新的名字和年龄。在 main 函数中,直接将 person1 传递给 updatePerson 函数,函数内部对 p 的修改会直接反映到 main 函数中的 person1 上。

常量引用参数传递

常量引用的概念

常量引用是指引用一个常量对象,通过常量引用传递参数,可以避免不必要的对象复制,同时保证在被调用函数中不会修改实参的值。这在传递大型对象或需要保证实参不被修改的场景下非常有用。

常量引用传递的优势

  1. 提高效率 对于大型对象,值传递会复制整个对象,而常量引用传递只传递对象的地址,大大提高了传递效率。例如,传递一个包含大量数据的 std::string 对象,如果采用值传递,复制操作可能会消耗较多时间和内存,而使用常量引用传递则能避免这一问题。
#include <iostream>
#include <string>

void printString(const std::string& str) {
    std::cout << "String is: " << str << std::endl;
}

int main() {
    std::string longString = "This is a very long string with a lot of characters...";
    printString(longString);
    return 0;
}

在上述代码中,printString 函数接受一个 const std::string& 类型的参数 str。通过常量引用传递 longString,避免了复制这个大型 std::string 对象,提高了程序的运行效率。

  1. 保证实参不被修改 在很多情况下,我们只需要在函数中读取实参的值,而不希望对其进行修改。使用常量引用传递参数可以在编译期就确保这一点,防止在函数内部意外修改实参。
#include <iostream>

void calculateSum(const int& a, const int& b) {
    // a++; // 这行代码会导致编译错误,因为a是常量引用
    std::cout << "Sum of " << a << " and " << b << " is: " << a + b << std::endl;
}

int main() {
    int num1 = 10;
    int num2 = 20;
    calculateSum(num1, num2);
    return 0;
}

在这个例子中,calculateSum 函数接受两个 const int& 类型的参数 ab。如果在函数内部尝试修改 ab,编译器会报错,从而保证了 num1num2main 函数中的值不会被意外修改。

右值引用与参数传递

右值引用的基本概念

右值引用是 C++11 引入的新特性,用于解决对象所有权转移和资源管理的问题。右值引用只能绑定到右值(临时对象、字面量等),与左值引用(可以绑定到左值,即具有名称和可寻址的对象)不同。

右值引用在参数传递中的应用

  1. 移动语义 右值引用在参数传递中最主要的应用是实现移动语义。移动语义允许我们在对象传递时,将资源(如动态分配的内存)从一个对象转移到另一个对象,而不是进行复制操作,从而提高效率。
#include <iostream>
#include <string>
#include <memory>

class MyString {
public:
    MyString() : data(nullptr), length(0) {}
    MyString(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        std::cout << "Copy constructor called" << std::endl;
    }
    MyString(MyString&& other) noexcept {
        data = other.data;
        length = other.length;
        other.data = nullptr;
        other.length = 0;
        std::cout << "Move constructor called" << std::endl;
    }
    ~MyString() {
        delete[] data;
    }
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            delete[] data;
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        std::cout << "Copy assignment operator called" << std::endl;
        return *this;
    }
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            length = other.length;
            other.data = nullptr;
            other.length = 0;
        }
        std::cout << "Move assignment operator called" << std::endl;
        return *this;
    }
    void print() {
        std::cout << "String: " << data << std::endl;
    }
private:
    char* data;
    size_t length;
};

MyString createString() {
    return MyString("Hello, World!");
}

void processString(MyString str) {
    str.print();
}

int main() {
    MyString s1 = createString();
    processString(std::move(s1));
    return 0;
}

在上述代码中,MyString 类定义了复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。createString 函数返回一个临时的 MyString 对象,这是一个右值。在 main 函数中,s1 通过移动构造函数从临时对象获取资源。然后,通过 std::moves1 转换为右值并传递给 processString 函数,此时也会调用移动构造函数,将 s1 的资源转移到 processString 函数的形参 str 中,避免了不必要的复制操作。

  1. 完美转发 右值引用还用于实现完美转发,即在函数模板中,能够将参数按照其原本的类型(左值或右值)转发给其他函数。这在编写通用库和元编程时非常有用。
#include <iostream>
#include <utility>

template <typename T>
void forwardFunction(T&& arg) {
    otherFunction(std::forward<T>(arg));
}

template <typename T>
void otherFunction(T arg) {
    std::cout << "otherFunction received: " << arg << std::endl;
}

int main() {
    int value = 10;
    forwardFunction(value);
    forwardFunction(20);
    return 0;
}

在这个例子中,forwardFunction 函数模板接受一个右值引用参数 arg,并通过 std::forwardarg 按照其原本的类型转发给 otherFunction。当 forwardFunction 传递左值 value 时,std::forward 会将其转发为左值;当传递右值 20 时,会将其转发为右值,从而保证在 otherFunction 中能够正确处理不同类型的参数。

函数参数传递的性能考量

值传递的性能分析

对于基本数据类型,值传递通常具有较高的性能,因为复制操作简单且快速。然而,对于大型对象,如包含大量成员变量或动态分配内存的结构体和类对象,值传递会带来较大的性能开销,因为需要复制整个对象及其内部的所有资源。例如,一个包含大量数据的 std::vector 对象,值传递时会复制整个 std::vector,包括其内部的所有元素,这会消耗较多的时间和内存。

指针传递和引用传递的性能优势

指针传递和引用传递在传递大型对象时具有明显的性能优势,因为它们只传递对象的地址,而不是整个对象。这大大减少了复制操作带来的开销。同时,指针传递和引用传递还能在被调用函数中修改调用函数的实参,提供了更大的灵活性。不过,指针传递需要手动管理指针,如指针的初始化、解引用和内存释放等,不当操作可能会导致内存泄漏或悬空指针等问题;而引用传递则相对更安全和简洁,语法上更接近值传递,同时又能实现高效传递和修改实参的功能。

右值引用在性能优化中的作用

右值引用通过实现移动语义和完美转发,为性能优化提供了强大的工具。移动语义允许在对象传递时避免不必要的复制操作,将资源从一个对象转移到另一个对象,从而提高效率。特别是在处理临时对象和动态分配资源的对象时,移动语义能够显著减少内存分配和释放的次数,提升程序的运行速度。完美转发则在函数模板中确保参数能够按照其原本的类型转发给其他函数,避免了不必要的类型转换和复制,进一步优化了性能,在编写通用库和元编程中具有重要意义。

函数参数传递的最佳实践

根据需求选择合适的传递方式

  1. 如果不需要修改实参 对于基本数据类型,值传递通常是最简单和高效的选择。例如,传递 intfloat 等类型的参数,值传递的复制操作开销较小。对于大型对象,如果只需要在函数中读取对象的值,不进行修改,可以使用常量引用传递,这样既能避免对象复制带来的性能开销,又能保证实参的安全性。

  2. 如果需要修改实参 对于基本数据类型和对象,可以选择指针传递或引用传递。引用传递语法更简洁,且避免了指针操作可能带来的错误,通常是更优先的选择。但在某些情况下,如需要处理动态分配内存的对象,指针传递可能更灵活,因为可以更方便地管理对象的生命周期。

利用右值引用优化性能

在 C++11 及以后的版本中,当涉及到对象所有权转移和资源管理时,应充分利用右值引用实现移动语义。特别是在函数返回临时对象或传递临时对象作为参数时,通过移动语义可以避免不必要的复制操作,提高程序的运行效率。同时,在编写通用函数模板时,合理使用右值引用和 std::forward 实现完美转发,确保参数能够按照其原本的类型传递,进一步优化性能。

避免不必要的对象复制

在设计函数接口时,应尽量避免不必要的对象复制。通过使用常量引用传递参数,以及在合适的情况下利用右值引用实现移动语义,可以显著减少对象复制带来的性能开销。同时,对于数组类型的参数,应了解其传递方式的特殊性,避免因误解而导致的性能问题或错误。例如,尽量使用 std::vector 替代传统数组,并采用合适的传递方式,如引用传递或常量引用传递,以提高性能和代码的安全性。

通过深入理解 C++ 函数的参数传递机制,并遵循这些最佳实践,可以编写出高效、健壮且易于维护的 C++ 代码。在实际编程中,应根据具体的需求和场景,灵活选择合适的参数传递方式,以达到最佳的性能和代码质量。同时,不断学习和掌握新的 C++ 特性,如右值引用和移动语义等,能够进一步提升编程能力和优化程序性能。