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

C++类普通成员函数的参数传递

2024-01-146.4k 阅读

C++ 类普通成员函数的参数传递基础概念

在 C++ 编程中,类是一种自定义的数据类型,它封装了数据(成员变量)和操作这些数据的函数(成员函数)。类的普通成员函数是定义在类内部的函数,用于对类的成员变量进行操作或者实现特定的功能。而参数传递则是向这些成员函数提供必要数据的方式,以便函数能够根据传入的数据执行相应的操作。

值传递

值传递是参数传递中最基本的方式。当使用值传递时,函数会创建一个参数的副本,函数内部对参数的任何修改都只影响这个副本,而不会影响原始的参数。

下面来看一个简单的示例代码:

#include <iostream>

class Example {
public:
    void modifyValue(int num) {
        num = num * 2;
        std::cout << "Inside modifyValue: num = " << num << std::endl;
    }
};

int main() {
    Example ex;
    int value = 10;
    ex.modifyValue(value);
    std::cout << "Back in main: value = " << value << std::endl;
    return 0;
}

在上述代码中,modifyValue 函数通过值传递接受一个 int 类型的参数 num。在函数内部,num 被修改为原来值的两倍。然而,当回到 main 函数中,我们可以看到 value 的值并没有改变。这是因为 numvalue 的一个副本,对 num 的修改不会影响 value

值传递的优点在于简单直接,对于一些简单的数据类型(如 intchar 等),它的效率较高,因为副本的创建开销相对较小。但对于大型对象,值传递可能会导致性能问题,因为创建对象副本需要消耗较多的时间和内存。

指针传递

指针传递允许函数通过指针来访问和修改原始数据。与值传递不同,指针传递不会创建参数的副本,而是传递指向原始数据的指针。

示例代码如下:

#include <iostream>

class Example {
public:
    void modifyValue(int* numPtr) {
        if (numPtr != nullptr) {
            *numPtr = *numPtr * 2;
            std::cout << "Inside modifyValue: *numPtr = " << *numPtr << std::endl;
        }
    }
};

int main() {
    Example ex;
    int value = 10;
    ex.modifyValue(&value);
    std::cout << "Back in main: value = " << value << std::endl;
    return 0;
}

在这个例子中,modifyValue 函数接受一个 int 类型的指针 numPtr。通过解引用指针 *numPtr,函数可以直接修改 main 函数中 value 的值。当回到 main 函数时,value 的值已经被成功修改。

指针传递的优点是对于大型对象,可以避免创建副本带来的性能开销,直接操作原始数据。但它也存在一些风险,比如指针可能为空,导致程序崩溃。因此,在使用指针传递时,必须小心检查指针是否为空。

引用传递

引用传递在很多方面类似于指针传递,但语法上更加简洁和安全。引用本质上是一个变量的别名,通过引用传递参数,函数可以直接操作原始数据,而不需要像指针那样进行解引用操作。

以下是引用传递的示例代码:

#include <iostream>

class Example {
public:
    void modifyValue(int& numRef) {
        numRef = numRef * 2;
        std::cout << "Inside modifyValue: numRef = " << numRef << std::endl;
    }
};

int main() {
    Example ex;
    int value = 10;
    ex.modifyValue(value);
    std::cout << "Back in main: value = " << value << std::endl;
    return 0;
}

在上述代码中,modifyValue 函数接受一个 int 类型的引用 numRef。在函数内部,对 numRef 的操作实际上就是对 main 函数中 value 的操作。所以当回到 main 函数时,value 的值已经被修改。

引用传递的优点在于它既有指针传递直接操作原始数据的能力,又避免了指针可能为空的风险,同时语法更加简洁直观。在传递大型对象时,引用传递也能避免值传递带来的性能问题,因此在很多情况下是一种非常理想的参数传递方式。

类对象作为参数传递

在 C++ 中,类对象也经常作为参数传递给普通成员函数。类对象的参数传递同样有值传递、指针传递和引用传递三种方式,每种方式都有其特点和适用场景。

类对象的值传递

当类对象通过值传递给成员函数时,会创建该对象的副本。这意味着函数内部对对象副本的任何修改都不会影响原始对象。

下面是一个具体的示例:

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : m_name(name) {}
    void changeName(const std::string& newName) {
        m_name = newName;
        std::cout << "Inside changeName: m_name = " << m_name << std::endl;
    }
private:
    std::string m_name;
};

int main() {
    Person person("Alice");
    person.changeName("Bob");
    std::cout << "Back in main: m_name = " << person.m_name << std::endl;
    return 0;
}

在上述代码中,Person 类有一个成员变量 m_namechangeName 函数通过值传递接受一个新的名字,并修改对象内部的 m_name。但这里需要注意的是,由于是值传递,changeName 函数修改的是对象的副本,原始的 person 对象的 m_name 并没有改变。

类对象值传递的缺点很明显,对于复杂的类对象,创建副本的开销较大,包括内存分配和成员变量的复制等操作。这可能会导致性能下降,尤其是在频繁调用函数的情况下。

类对象的指针传递

通过指针传递类对象,函数可以直接访问和修改原始对象。这就避免了值传递中创建副本的开销。

示例代码如下:

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : m_name(name) {}
    void changeName(const std::string& newName) {
        m_name = newName;
        std::cout << "Inside changeName: m_name = " << m_name << std::endl;
    }
private:
    std::string m_name;
};

int main() {
    Person* person = new Person("Alice");
    person->changeName("Bob");
    std::cout << "Back in main: m_name = " << person->m_name << std::endl;
    delete person;
    return 0;
}

在这个例子中,main 函数创建了一个 Person 类的指针 person,并通过指针调用 changeName 函数。由于是指针传递,changeName 函数可以直接修改原始对象的 m_name。在使用完指针后,别忘了通过 delete 释放内存,以避免内存泄漏。

类对象指针传递的优点是效率高,能直接操作原始对象。但同样存在指针为空的风险,在调用成员函数之前需要确保指针有效。

类对象的引用传递

引用传递类对象结合了指针传递的高效性和值传递的语法简洁性,同时避免了指针为空的风险。

以下是示例代码:

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : m_name(name) {}
    void changeName(const std::string& newName) {
        m_name = newName;
        std::cout << "Inside changeName: m_name = " << m_name << std::endl;
    }
private:
    std::string m_name;
};

int main() {
    Person person("Alice");
    Person& refPerson = person;
    refPerson.changeName("Bob");
    std::cout << "Back in main: m_name = " << person.m_name << std::endl;
    return 0;
}

在上述代码中,refPersonperson 的引用,通过引用调用 changeName 函数,实际上就是对原始 person 对象进行操作。这样既避免了创建副本的开销,又不需要担心引用为空的问题。

在实际编程中,对于类对象的参数传递,引用传递通常是首选方式,除非有特殊需求,如需要传递空指针等情况才会考虑指针传递,而值传递一般用于简单的、小型的类对象或者需要保持原始对象不变的场景。

常量参数传递

在 C++ 中,为了防止函数内部意外修改传递进来的参数,我们可以使用常量参数传递。无论是基本数据类型、指针还是引用,都可以声明为常量。

基本数据类型的常量参数

对于基本数据类型,使用常量参数传递主要是为了明确函数不会修改该参数的值,同时也能防止一些不必要的类型转换。

示例代码如下:

#include <iostream>

class Example {
public:
    void printValue(const int num) {
        // num = num + 1; // 这行代码会导致编译错误,因为 num 是常量
        std::cout << "The value is: " << num << std::endl;
    }
};

int main() {
    Example ex;
    int value = 10;
    ex.printValue(value);
    return 0;
}

在上述代码中,printValue 函数接受一个 const int 类型的参数 num。在函数内部,试图修改 num 的值会导致编译错误,这样就保证了 num 在函数内部不会被意外修改。

指针的常量参数

当使用指针传递参数时,我们可以有两种类型的常量声明:常量指针和指向常量的指针。

常量指针

常量指针是指指针本身的值不能被修改,即指针始终指向同一个内存地址,但可以修改指针所指向的值。

示例代码如下:

#include <iostream>

class Example {
public:
    void modifyValue(int* const numPtr) {
        if (numPtr != nullptr) {
            *numPtr = *numPtr * 2;
            std::cout << "Inside modifyValue: *numPtr = " << *numPtr << std::endl;
        }
    }
};

int main() {
    Example ex;
    int value1 = 10;
    int value2 = 20;
    int* ptr = &value1;
    ex.modifyValue(ptr);
    // ptr = &value2; // 这行代码会导致编译错误,因为 ptr 是常量指针
    std::cout << "Back in main: value1 = " << value1 << std::endl;
    return 0;
}

在这个例子中,modifyValue 函数接受一个 int* const 类型的常量指针 numPtr。函数内部可以修改 numPtr 所指向的值,但不能修改 numPtr 本身,即不能让它指向其他地址。

指向常量的指针

指向常量的指针则相反,指针可以指向不同的内存地址,但不能修改指针所指向的值。

示例代码如下:

#include <iostream>

class Example {
public:
    void printValue(const int* numPtr) {
        if (numPtr != nullptr) {
            std::cout << "The value is: " << *numPtr << std::endl;
        }
    }
};

int main() {
    Example ex;
    int value1 = 10;
    int value2 = 20;
    const int* ptr = &value1;
    ex.printValue(ptr);
    ptr = &value2;
    ex.printValue(ptr);
    // *ptr = 30; // 这行代码会导致编译错误,因为 ptr 指向常量
    return 0;
}

在上述代码中,printValue 函数接受一个 const int* 类型的指向常量的指针 numPtr。函数内部可以让 ptr 指向不同的值,但不能修改 ptr 所指向的值。

引用的常量参数

对于引用传递,使用常量引用可以防止函数内部修改传递进来的对象,同时避免了值传递的副本开销。

示例代码如下:

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : m_name(name) {}
    void printName(const std::string& nameRef) const {
        std::cout << "The name is: " << nameRef << std::endl;
    }
private:
    std::string m_name;
};

int main() {
    Person person("Alice");
    std::string name = "Bob";
    person.printName(name);
    return 0;
}

在这个例子中,printName 函数接受一个 const std::string& 类型的常量引用 nameRef。函数内部不能修改 nameRef,同时由于是引用传递,避免了复制 std::string 对象带来的开销。

常量参数传递在函数接口设计中非常重要,它可以提高代码的安全性和可读性,让调用者清楚知道函数是否会修改传递进来的参数。

数组作为参数传递

在 C++ 中,数组作为参数传递给类的普通成员函数有其独特的方式和特点。

一维数组作为参数

当一维数组作为参数传递给成员函数时,实际上传递的是数组的首地址,也就是一个指针。

示例代码如下:

#include <iostream>

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

int main() {
    Example ex;
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    ex.printArray(arr, size);
    return 0;
}

在上述代码中,printArray 函数接受一个 int 类型的数组 arr 和数组的大小 size。这里的 arr 实际上是一个指向数组首元素的指针。由于数组名在作为参数传递时会衰减为指针,所以函数内部无法通过 arr 直接获取数组的大小,需要额外传递数组大小参数。

二维数组作为参数

二维数组作为参数传递给成员函数时,同样传递的是数组的首地址,但需要注意数组的维度声明。

示例代码如下:

#include <iostream>

class Example {
public:
    void print2DArray(int arr[][3], int rows) {
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < 3; ++j) {
                std::cout << arr[i][j] << " ";
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    Example ex;
    int arr[][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
    int rows = sizeof(arr) / sizeof(arr[0]);
    ex.print2DArray(arr, rows);
    return 0;
}

在这个例子中,print2DArray 函数接受一个二维数组 arr 和行数 rows。需要注意的是,二维数组作为参数时,除了第一维可以省略,其他维度必须明确指定,因为编译器需要知道每行的元素个数来正确计算内存地址。

动态数组作为参数

动态数组通常是通过 new 运算符在堆上分配内存创建的。当动态数组作为参数传递给成员函数时,同样传递的是数组的首地址。

示例代码如下:

#include <iostream>

class Example {
public:
    void printDynamicArray(int* arr, int size) {
        for (int i = 0; i < size; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    Example ex;
    int size = 5;
    int* dynamicArr = new int[size];
    for (int i = 0; i < size; ++i) {
        dynamicArr[i] = i + 1;
    }
    ex.printDynamicArray(dynamicArr, size);
    delete[] dynamicArr;
    return 0;
}

在上述代码中,main 函数创建了一个动态数组 dynamicArr,并将其首地址和大小传递给 printDynamicArray 函数。在使用完动态数组后,别忘了通过 delete[] 释放内存,以避免内存泄漏。

数组作为参数传递时,由于传递的是指针,所以在函数内部对数组的修改会影响原始数组。同时,要注意数组大小的管理,确保在访问数组元素时不会越界。

函数指针作为参数传递

在 C++ 中,函数指针可以作为参数传递给类的普通成员函数,这为程序提供了更高的灵活性和可扩展性。

函数指针的基本概念

函数指针是指向函数的指针变量,它存储了函数的入口地址。通过函数指针,可以像调用普通函数一样调用它所指向的函数。

示例代码如下:

#include <iostream>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

void operate(int a, int b, int (*func)(int, int)) {
    int result = func(a, b);
    std::cout << "The result is: " << result << std::endl;
}

int main() {
    operate(5, 3, add);
    operate(5, 3, subtract);
    return 0;
}

在上述代码中,addsubtract 是两个普通函数,operate 函数接受两个整数和一个函数指针 func。通过传递不同的函数指针,operate 函数可以执行不同的操作。

函数指针作为类成员函数参数

当函数指针作为类的普通成员函数参数时,同样可以实现灵活的功能调用。

示例代码如下:

#include <iostream>

class MathOperations {
public:
    int add(int a, int b) {
        return a + b;
    }

    int subtract(int a, int b) {
        return a - b;
    }

    void operate(int a, int b, int (MathOperations::*func)(int, int)) {
        int result = (this->*func)(a, b);
        std::cout << "The result is: " << result << std::endl;
    }
};

int main() {
    MathOperations mo;
    mo.operate(5, 3, &MathOperations::add);
    mo.operate(5, 3, &MathOperations::subtract);
    return 0;
}

在这个例子中,MathOperations 类有 addsubtract 两个成员函数,operate 成员函数接受两个整数和一个指向类成员函数的指针 func。通过 (this->*func)(a, b) 的方式,可以调用相应的成员函数。

函数指针作为参数传递使得代码可以根据不同的需求动态选择要执行的函数,在实现一些通用算法或者回调机制时非常有用。但使用函数指针也增加了代码的复杂性,需要小心处理函数指针的类型匹配和调用。

模板参数传递

C++ 的模板机制为参数传递提供了更强大的功能,可以实现通用的算法和数据结构,而不需要为每种数据类型都编写重复的代码。

函数模板参数传递

函数模板允许我们编写一个通用的函数,该函数可以处理不同的数据类型。

示例代码如下:

#include <iostream>

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int result1 = add(5, 3);
    double result2 = add(5.5, 3.5);
    return 0;
}

在上述代码中,add 是一个函数模板,通过 template <typename T> 声明了一个类型参数 T。在函数定义中,T 可以代表任何数据类型。当调用 add 函数时,编译器会根据传递的参数类型自动实例化相应的函数版本。

类模板参数传递

类模板同样允许我们创建通用的类,其中的成员函数也可以使用模板参数。

示例代码如下:

#include <iostream>

template <typename T>
class Stack {
public:
    Stack() : top(-1) {}
    void push(T value) {
        if (top < MAX_SIZE - 1) {
            data[++top] = value;
        }
    }
    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        return T();
    }
private:
    static const int MAX_SIZE = 100;
    T data[MAX_SIZE];
    int top;
};

int main() {
    Stack<int> intStack;
    intStack.push(10);
    int value1 = intStack.pop();

    Stack<double> doubleStack;
    doubleStack.push(10.5);
    double value2 = doubleStack.pop();
    return 0;
}

在这个例子中,Stack 是一个类模板,通过 template <typename T> 定义了类型参数 TStack 类的成员函数 pushpop 可以处理任何类型 T 的数据。通过实例化 Stack<int>Stack<double>,可以创建不同类型的栈。

模板参数传递极大地提高了代码的复用性和灵活性,但模板的使用也可能导致编译时间变长和错误信息难以理解,因此在使用时需要权衡利弊。

右值引用参数传递

C++11 引入了右值引用的概念,它为参数传递带来了新的优化方式,特别是在处理临时对象时。

右值引用的基本概念

右值引用是对右值(临时对象)的引用,通过 && 符号声明。与左值引用不同,右值引用允许我们移动而不是复制临时对象,从而提高性能。

示例代码如下:

#include <iostream>
#include <string>

class Example {
public:
    Example(const std::string& str) : m_str(str) {
        std::cout << "Constructor with lvalue reference" << std::endl;
    }

    Example(std::string&& str) noexcept : m_str(std::move(str)) {
        std::cout << "Constructor with rvalue reference" << std::endl;
    }

    ~Example() {
        std::cout << "Destructor" << std::endl;
    }

private:
    std::string m_str;
};

Example createExample() {
    std::string temp = "Hello";
    return Example(temp);
}

int main() {
    Example ex1 = createExample();
    Example ex2 = std::move(ex1);
    return 0;
}

在上述代码中,Example 类有两个构造函数,一个接受左值引用,另一个接受右值引用。createExample 函数返回一个临时的 Example 对象,在 main 函数中,ex1 通过右值引用构造函数进行初始化,避免了不必要的复制。ex2 通过 std::moveex1 的资源移动过来,进一步优化了性能。

右值引用作为成员函数参数

右值引用也可以作为类的普通成员函数参数,用于实现移动语义,提高对象操作的效率。

示例代码如下:

#include <iostream>
#include <string>

class Example {
public:
    Example(const std::string& str) : m_str(str) {}

    void setValue(std::string&& str) noexcept {
        m_str = std::move(str);
        std::cout << "setValue with rvalue reference" << std::endl;
    }

    void printValue() const {
        std::cout << "m_str = " << m_str << std::endl;
    }

private:
    std::string m_str;
};

int main() {
    Example ex("Initial value");
    std::string temp = "New value";
    ex.setValue(std::move(temp));
    ex.printValue();
    return 0;
}

在这个例子中,setValue 函数接受一个右值引用参数 str,通过 std::movestr 的资源移动到 m_str,避免了复制操作。这在处理大型对象时可以显著提高性能。

右值引用参数传递为 C++ 程序员提供了一种更高效地处理临时对象的方式,在现代 C++ 编程中得到了广泛应用。但需要注意的是,使用右值引用时要确保移动操作的安全性和正确性。

总结

C++ 类普通成员函数的参数传递方式丰富多样,每种方式都有其特点、适用场景和优缺点。值传递简单直接,但对于大型对象可能导致性能问题;指针传递和引用传递能直接操作原始数据,提高效率,但指针传递存在空指针风险,而引用传递语法更简洁安全。类对象作为参数传递时,引用传递通常是首选。常量参数传递可提高代码安全性,数组、函数指针、模板参数以及右值引用参数传递则为编程带来了更多的灵活性和优化空间。在实际编程中,需要根据具体需求和场景,仔细选择合适的参数传递方式,以编写高效、安全和可维护的代码。通过深入理解和熟练运用这些参数传递方式,程序员能够更好地驾驭 C++ 语言,实现复杂而高效的程序逻辑。