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

C++按值传递在不同数据类型的表现

2023-08-172.8k 阅读

C++按值传递在基础数据类型中的表现

整数类型

在C++ 中,整数类型包括 charshortintlonglong long 等。当使用按值传递将一个整数类型的变量传递给函数时,实际传递的是该变量值的副本。

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

#include <iostream>

void increment(int num) {
    num++;
    std::cout << "函数内部 num 的值: " << num << std::endl;
}

int main() {
    int value = 10;
    std::cout << "调用函数前 value 的值: " << value << std::endl;
    increment(value);
    std::cout << "调用函数后 value 的值: " << value << std::endl;
    return 0;
}

在上述代码中,increment 函数接收一个 int 类型的参数 num。在函数内部对 num 进行自增操作。由于是按值传递,numvalue 的副本,对 num 的修改不会影响到 main 函数中的 value。所以在函数调用前后,value 的值保持不变。

浮点数类型

浮点数类型如 floatdouble 在按值传递时,同样传递的是值的副本。

示例代码如下:

#include <iostream>

void multiply(float num) {
    num *= 2;
    std::cout << "函数内部 num 的值: " << num << std::endl;
}

int main() {
    float value = 3.14f;
    std::cout << "调用函数前 value 的值: " << value << std::endl;
    multiply(value);
    std::cout << "调用函数后 value 的值: " << value << std::endl;
    return 0;
}

在这个例子中,multiply 函数接收一个 float 类型的参数 num,并将其值翻倍。但由于 numvalue 的副本,main 函数中的 value 值不会被改变。

布尔类型

布尔类型 bool 按值传递时也是传递副本。

以下是示例代码:

#include <iostream>

void toggle(bool flag) {
    flag =!flag;
    std::cout << "函数内部 flag 的值: " << flag << std::endl;
}

int main() {
    bool value = true;
    std::cout << "调用函数前 value 的值: " << value << std::endl;
    toggle(value);
    std::cout << "调用函数后 value 的值: " << value << std::endl;
    return 0;
}

toggle 函数接收一个 bool 类型的参数 flag 并取反。然而,这并不会影响 main 函数中 value 的值,因为传递的是副本。

C++按值传递在数组类型中的表现

一维数组

在C++ 中,当将一维数组按值传递给函数时,实际上传递的并不是整个数组的副本,而是数组首元素的指针。这与按值传递的概念有所不同,但在C++ 语言设计中,这种处理方式是为了提高效率,避免大量数据的复制。

示例代码如下:

#include <iostream>

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

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

在上述代码中,printArray 函数接收一个 int 类型的数组参数 arr。实际上,arr 是一个指向 numbers 数组首元素的指针。虽然看起来像是按值传递数组,但本质上传递的是指针。

二维数组

对于二维数组,按值传递时同样传递的是数组首元素的指针。但二维数组的指针表示更为复杂。

示例代码如下:

#include <iostream>

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

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print2DArray(matrix);
    return 0;
}

在这个例子中,print2DArray 函数接收一个二维数组 arr。这里 arr 同样是指向 matrix 数组首元素的指针,只是在访问元素时需要按照二维数组的方式进行索引。

C++按值传递在结构体类型中的表现

简单结构体

当结构体按值传递时,整个结构体的内容会被复制。

示例代码如下:

#include <iostream>

struct Point {
    int x;
    int y;
};

void move(Point p) {
    p.x += 10;
    p.y += 10;
    std::cout << "函数内部点的坐标: (" << p.x << ", " << p.y << ")" << std::endl;
}

int main() {
    Point origin = {0, 0};
    std::cout << "调用函数前点的坐标: (" << origin.x << ", " << origin.y << ")" << std::endl;
    move(origin);
    std::cout << "调用函数后点的坐标: (" << origin.x << ", " << origin.y << ")" << std::endl;
    return 0;
}

在上述代码中,Point 结构体包含两个 int 类型的成员变量 xymove 函数接收一个 Point 类型的结构体参数 p,并对其坐标进行移动。由于是按值传递,porigin 的副本,对 p 的修改不会影响 origin

包含数组的结构体

如果结构体中包含数组,按值传递时数组的内容也会被复制。

示例代码如下:

#include <iostream>

struct Data {
    int values[5];
};

void process(Data d) {
    for (int i = 0; i < 5; ++i) {
        d.values[i] *= 2;
    }
    std::cout << "函数内部数组的值: ";
    for (int i = 0; i < 5; ++i) {
        std::cout << d.values[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    Data info = {1, 2, 3, 4, 5};
    std::cout << "调用函数前数组的值: ";
    for (int i = 0; i < 5; ++i) {
        std::cout << info.values[i] << " ";
    }
    std::cout << std::endl;
    process(info);
    std::cout << "调用函数后数组的值: ";
    for (int i = 0; i < 5; ++i) {
        std::cout << info.values[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

在这个例子中,Data 结构体包含一个 int 类型的数组 valuesprocess 函数接收一个 Data 结构体参数 d,并对数组元素进行翻倍操作。由于按值传递,dinfo 的副本,info 中的数组值不会被改变。

C++按值传递在类类型中的表现

简单类

当类对象按值传递时,会调用类的拷贝构造函数来创建对象的副本。

示例代码如下:

#include <iostream>

class Circle {
public:
    Circle(int r) : radius(r) {}
    Circle(const Circle& other) : radius(other.radius) {
        std::cout << "拷贝构造函数被调用" << std::endl;
    }
    ~Circle() {}
    void setRadius(int r) { radius = r; }
    int getRadius() const { return radius; }
private:
    int radius;
};

void scale(Circle c) {
    c.setRadius(c.getRadius() * 2);
    std::cout << "函数内部圆的半径: " << c.getRadius() << std::endl;
}

int main() {
    Circle myCircle(5);
    std::cout << "调用函数前圆的半径: " << myCircle.getRadius() << std::endl;
    scale(myCircle);
    std::cout << "调用函数后圆的半径: " << myCircle.getRadius() << std::endl;
    return 0;
}

在上述代码中,Circle 类有一个构造函数、一个拷贝构造函数和相关的成员函数。scale 函数接收一个 Circle 类型的对象 c。当 myCircle 传递给 scale 函数时,拷贝构造函数被调用创建 ccmyCircle 的副本,对 c 的修改不会影响 myCircle

包含动态内存的类

如果类中包含动态分配的内存,按值传递时需要特别注意。默认的拷贝构造函数会进行浅拷贝,这可能导致内存泄漏等问题。

示例代码如下:

#include <iostream>
#include <cstring>

class String {
public:
    String(const char* str = nullptr) {
        if (str == nullptr) {
            this->str = nullptr;
            this->length = 0;
        } else {
            this->length = strlen(str);
            this->str = new char[this->length + 1];
            strcpy(this->str, str);
        }
    }
    String(const String& other) {
        this->length = other.length;
        this->str = new char[this->length + 1];
        strcpy(this->str, other.str);
        std::cout << "深拷贝构造函数被调用" << std::endl;
    }
    ~String() {
        if (this->str != nullptr) {
            delete[] this->str;
        }
    }
    void print() const {
        std::cout << "字符串: " << this->str << std::endl;
    }
private:
    char* str;
    int length;
};

void append(String s) {
    char newStr[100];
    strcpy(newStr, s.str);
    strcat(newStr, " appended");
    s.str = new char[strlen(newStr) + 1];
    strcpy(s.str, newStr);
    s.print();
}

int main() {
    String myString("Hello");
    std::cout << "调用函数前字符串: ";
    myString.print();
    append(myString);
    std::cout << "调用函数后字符串: ";
    myString.print();
    return 0;
}

在这个例子中,String 类动态分配内存来存储字符串。自定义的拷贝构造函数进行深拷贝,确保每个对象有自己独立的内存空间。append 函数接收一个 String 对象 s,对其进行字符串追加操作,但由于按值传递,myString 不受影响。

C++按值传递在指针类型中的表现

普通指针

当普通指针按值传递时,传递的是指针值的副本,即另一个指向相同内存地址的指针。

示例代码如下:

#include <iostream>

void increment(int* ptr) {
    (*ptr)++;
    std::cout << "函数内部指针指向的值: " << *ptr << std::endl;
}

int main() {
    int num = 10;
    int* pointer = &num;
    std::cout << "调用函数前指针指向的值: " << *pointer << std::endl;
    increment(pointer);
    std::cout << "调用函数后指针指向的值: " << *pointer << std::endl;
    return 0;
}

在上述代码中,increment 函数接收一个 int 类型的指针 ptr。由于传递的是指针值的副本,ptrpointer 指向同一个 int 变量 num。对 ptr 指向的值进行自增操作,会影响到 num 的值。

指向结构体的指针

当指向结构体的指针按值传递时,同样传递的是指针值的副本。

示例代码如下:

#include <iostream>

struct Rectangle {
    int width;
    int height;
};

void resize(Rectangle* rect) {
    rect->width *= 2;
    rect->height *= 2;
    std::cout << "函数内部矩形的宽和高: (" << rect->width << ", " << rect->height << ")" << std::endl;
}

int main() {
    Rectangle myRect = {5, 10};
    Rectangle* rectPtr = &myRect;
    std::cout << "调用函数前矩形的宽和高: (" << rectPtr->width << ", " << rectPtr->height << ")" << std::endl;
    resize(rectPtr);
    std::cout << "调用函数后矩形的宽和高: (" << rectPtr->width << ", " << rectPtr->height << ")" << std::endl;
    return 0;
}

在这个例子中,resize 函数接收一个指向 Rectangle 结构体的指针 rect。由于传递的是指针值的副本,rectrectPtr 指向同一个 Rectangle 对象,对 rect 所指结构体成员的修改会影响到 myRect

C++按值传递在引用类型中的表现

普通引用

C++ 中的引用本质上是变量的别名,当使用按值传递传递引用时,实际上传递的是引用所绑定的变量值的副本。

示例代码如下:

#include <iostream>

void increment(int& num) {
    num++;
    std::cout << "函数内部 num 的值: " << num << std::endl;
}

int main() {
    int value = 10;
    int& ref = value;
    std::cout << "调用函数前 value 的值: " << value << std::endl;
    increment(ref);
    std::cout << "调用函数后 value 的值: " << value << std::endl;
    return 0;
}

在上述代码中,increment 函数接收一个 int 类型的引用 num。虽然看起来是按值传递引用,但实际上 numvalue 的别名,对 num 的修改会直接影响 value

结构体引用

当传递结构体引用时,情况类似。传递的是结构体对象的别名。

示例代码如下:

#include <iostream>

struct Point {
    int x;
    int y;
};

void move(Point& p) {
    p.x += 10;
    p.y += 10;
    std::cout << "函数内部点的坐标: (" << p.x << ", " << p.y << ")" << std::endl;
}

int main() {
    Point origin = {0, 0};
    Point& ref = origin;
    std::cout << "调用函数前点的坐标: (" << ref.x << ", " << ref.y << ")" << std::endl;
    move(ref);
    std::cout << "调用函数后点的坐标: (" << ref.x << ", " << ref.y << ")" << std::endl;
    return 0;
}

在这个例子中,move 函数接收一个 Point 结构体的引用 pporigin 的别名,对 p 的修改会影响 origin

C++按值传递的性能考量

基础数据类型的性能

对于基础数据类型,如整数、浮点数和布尔类型,按值传递的性能开销相对较小。因为这些数据类型的大小通常较小,复制它们的副本不会占用大量的时间和空间。例如,int 类型通常占用 4 个字节,在现代计算机硬件上,复制 4 个字节的数据是非常快速的操作。

复杂数据类型的性能

结构体和类

对于结构体和类,如果它们的成员变量较多或者包含动态分配的内存,按值传递可能会带来较大的性能开销。因为按值传递需要复制整个结构体或类对象的内容。例如,一个包含大量成员变量的结构体或者一个包含动态分配大数组的类,复制它们的副本会消耗较多的时间和内存。

数组

虽然数组按值传递实际传递的是指针,但如果需要访问整个数组的内容,在函数内部操作数组可能会带来一定的缓存不命中问题。特别是对于大型数组,由于内存空间的连续性和缓存机制的影响,频繁访问数组元素可能导致性能下降。

按值传递与其他传递方式的对比

按值传递与按引用传递

按值传递创建变量的副本,对副本的修改不会影响原始变量。而按引用传递是传递变量的别名,对引用的修改会直接影响原始变量。从性能角度看,对于大型结构体和类,按引用传递可以避免对象的复制,提高性能。但按引用传递可能会导致函数对原始数据的意外修改,而按值传递则提供了数据的保护。

按值传递与按指针传递

按值传递指针时,传递的是指针值的副本,这与按指针传递本身有些相似。但按值传递指针仍然有一定的安全性,因为即使在函数内部修改指针副本所指向的值,不会影响到指针本身的地址(除非进行特殊的指针运算)。而直接按指针传递,如果在函数内部不小心修改了指针的地址,可能会导致程序出现难以调试的错误。

总结按值传递的适用场景

数据保护

当函数不需要修改传递进来的数据,并且希望确保原始数据不被意外修改时,按值传递是一个很好的选择。例如,一个用于打印数据的函数,只需要读取数据,按值传递可以防止函数内部意外修改数据。

简单数据类型

对于基础数据类型和小型结构体,按值传递的性能开销较小,并且代码逻辑清晰。直接传递值可以避免使用引用或指针带来的复杂性。

临时数据处理

当函数需要处理临时数据,并且不需要保留修改结果时,按值传递可以简化代码。例如,一个用于计算两个数之和并返回结果的函数,接收按值传递的参数即可。

在C++ 编程中,深入理解按值传递在不同数据类型中的表现,对于编写高效、健壮的代码至关重要。根据不同的需求和数据类型的特点,合理选择传递方式,可以提高程序的性能和可维护性。