秋招C++八股--指针和引用(持续更新 v3)

1 C++中的指针参数传递和引用参数传递有什么区别?底层原理是?

1) 指针参数传递本质上是值传递,它所传递的是一个地址值。

值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址

被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

因此,被调函数对形参的任何操作都会影响主调函数中的实参变量

3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引 用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。

符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

#include <iostream>

// 通过引用传递修改主调函数中的变量
void modifyByReference(int& value) {
    value = 10;
}

// 通过指针传递无法修改主调函数中的指针变量
void modifyByPointer(int* ptr) {
    ptr = nullptr;
}

int main() {
    int num = 5;
    int* ptr = &num;

    std::cout << "Before modification:" << std::endl;
    std::cout << "num: " << num << std::endl;
    std::cout << "ptr: " << ptr << std::endl;

    // 通过引用传递修改变量
    modifyByReference(num);

    // 通过指针传递尝试修改指针变量
    modifyByPointer(ptr);

    std::cout << "After modification:" << std::endl;
    std::cout << "num: " << num << std::endl;
    std::cout << "ptr: " << ptr << std::endl;

    return 0;
}
Before modification:
num: 5
ptr: 0x7ffee97596ec
After modification:
num: 10
ptr: 0x7ffee97596ec

2 智能指针

  1. shared_ptr:

shared_ptr使用引用计数的方式管理资源,允许多个智能指针共享同一个对象。每当有一个新的智能指针指向该对象时,引用计数会增加1;每当智能指针不再指向该对象时,引用计数会减少1。当引用计数减至0时,智能指针会自动释放动态分配的资源。

  1. unique_ptr:

unique_ptr采用独享所有权的语义,一个非空的unique_ptr始终拥有它所指向的资源。它不支持普通的拷贝和赋值操作,因此不能用于STL标准容器,除非通过移动语义将所有权转移给另一个unique_ptr。当unique_ptr被销毁或重置时,它会自动释放所拥有的资源。

  1. weak_ptr:

weak_ptr是一种弱引用,它用于解决shared_ptr可能形成的循环引用问题。weak_ptr指向由shared_ptr管理的对象,但不增加引用计数。当所有shared_ptr析构后,即使还有weak_ptr引用该对象,内存也会被释放。为了使用weak_ptr,可以使用其成员函数lock()检查是否指向有效的内存。

  1. auto_ptr(已被废弃):

auto_ptr是C++98标准中引入的智能指针,主要用于解决异常发生时可能导致内存泄漏的问题。然而,auto_ptr存在一些问题,包括拷贝语义导致源对象变为无效和不能在STL容器中使用。由于这些问题,C++11引入了更安全和功能更强大的unique_ptr来替代auto_ptr。

#include <iostream>

template<typename T>
class SharedPtr {
public:
    SharedPtr() : ptr(nullptr), refCount(nullptr) {}  // 默认构造函数,初始化指针和引用计数为nullptr

    // 不能进行隐式类型转换,调用者必须使用显式构造函数:
    explicit SharedPtr(T* ptr) : ptr(ptr), refCount(new size_t(1)) {}  // 显式构造函数,传入指针并创建引用计数

    SharedPtr(const SharedPtr& other) : ptr(other.ptr), refCount(other.refCount) {  // 拷贝构造函数
        if (refCount) {
            (*refCount)++;  // 增加引用计数
        }
    }
    // 1 首先,代码检查是否发生了自我赋值,即当前对象是否被赋值给自身。
    //   如果发生了自我赋值,直接返回当前对象的引用,不做任何操作,以避免资源的误释放或其他不必要的问题。
    // 2 在进行实际赋值操作之前,通过调用 release() 函数,释放当前对象所持有的资源(例如,释放内存和引用计数)。
    //   这是一个非常重要的步骤,因为在重新赋值之前,我们必须确保不会造成资源泄漏。
    //   返回 this 是指向当前对象的指针,而不是当前对象本身。
    SharedPtr& operator=(const SharedPtr& other) {  // 赋值运算符重载

        // 为了避免潜在的问题,一种常见的做法是直接返回当前对象的引用
        if (this == &other) {  // 检查自我赋值
            return *this;
        }

        release();  // 释放当前指针和引用计数

        ptr = other.ptr;  // 复制指针
        refCount = other.refCount;  // 复制引用计数

        if (refCount) {
            (*refCount)++;  // 增加引用计数
        }

        return *this;
    }

    ~SharedPtr() {  // 析构函数
        release();  // 释放指针和引用计数
    }
    // 返回 T 的引用类型 --> 可以对其进行修改
    // 对 * 进行运算符重载
    // 该函数是一个常量成员函数,意味着它不能修改调用对象的成员变量
    T& operator*() const {  // 解引用运算符重载
        return *ptr;
    }

    // 返回 T 指针类型
    T* operator->() const {  // 指针访问运算符重载
        return ptr;
    }

    size_t useCount() const {  // 返回引用计数值
        return refCount ? *refCount : 0;
    }

private:
    void release() {  // 释放指针和引用计数
        if (refCount) {
            (*refCount)--;  // 减少引用计数
            if (*refCount == 0) {
                delete ptr;  // 释放指针指向的对象
                delete refCount;  // 释放引用计数
            }
        }
    }

    T* ptr;  // 指向被共享对象的指针
    size_t* refCount;  // 指向引用计数的指针
};

int main() {
    SharedPtr<int> sp1(new int(42));  // 创建SharedPtr,初始化指向整数42的指针
    SharedPtr<int> sp2 = sp1;  // 使用拷贝构造函数创建新的SharedPtr,指向同一个对象

    std::cout << "sp1 use count: " << sp1.useCount() << std::endl;  // 输出:2,显示引用计数为2
    std::cout << "sp2 use count: " << sp2.useCount() << std::endl;  // 输出:2,显示引用计数为2

    *sp1 = 100;  // 修改被共享对象的值

    std::cout << "sp1 use count: " << sp1.useCount() << std::endl;  // 输出:2,显示引用计数为2
    std::cout << "sp2 use count: " << sp2.useCount() << std::endl;  // 输出:2,显示引用计数为2

    SharedPtr<int> sp3;  // 创建一个空的SharedPtr
    sp3 = sp1;  // 赋值运算符重载,将sp1的引用赋给sp3

    std::cout << "sp1 use count: " << sp1.useCount() << std::endl;  // 输出:3,显示引用计数为3
    std::cout << "sp3 use count: " << sp3.useCount() << std::endl;  // 输出:3,显示引用计数为3

    return 0;
}


这段代码实现了一个简单的智能指针 SharedPtr。它使用引用计数的方法管理动态分配的资源

每个 SharedPtr 对象包含一个指向动态分配对象的指针 ptr 和一个指向引用计数的指针 reCcount

在构造函数中,引用计数被初始化为1,表示有一个指针指向资源。

当有多个 SharedPtr 对象指向同一个资源时,它们共享同一个引用计数

当对象被拷贝构造或赋值给另一个对象时,引用计数增加

当对象被析构时,引用计数减少,如果引用计数减至0,则释放资源。

这样可以确保资源在没有引用的情况下被正确释放,避免了内存泄漏。

同时,通过重载 *-> 运算符,可以以类似指针的方式访问所管理的对象。

1.shared_ptr怎么实现多指针指向同一个地址 ?

shared_ptr 实现多个指针指向同一个地址的关键在于引用计数。在 SharedPtr 类中,通过维护一个指向引用计数的指针 _pcount,多个 SharedPtr 对象共享同一个 _pcount,从而实现引用计数的共享。当有新的 SharedPtr 对象拷贝构造或赋值给已有的 SharedPtr 对象时,引用计数会增加,当 SharedPtr 对象被析构或赋予新值时,引用计数会减少。只有当引用计数为 0 时,才会释放所指向的资源。

2.引用计数如何保证不同类实例的指针之间共享同步 ?

引用计数通过共享 _pcount 实现不同类实例的指针之间的引用计数共享。当多个 SharedPtr 对象指向同一个资源时,它们共享同一个 _pcount 指针。引用计数的增加和减少都是通过修改 _pcount 指向的值来实现的,因此不同类实例的指针之间可以共享同步的引用计数。

3 指针和引用的区别

指针和引用的主要区别如下:

  1. 存储方式:指针是一个变量,存储的是一个地址;引用是原变量的别名,实质上是同一个东西。
  2. 级别:指针可以有多级,可以通过多级指针访问多层嵌套的数据结构;引用只有一级。
  3. 空值和初始化:指针可以为空,即指向空地址或NULL;引用不能为NULL且在定义时必须初始化。
  4. 指向的可变性:指针在初始化后可以改变指向不同的地址;引用在初始化之后不可再改变指向的变量。
  5. sizeof运算符的结果:sizeof指针得到的是本指针的大小;sizeof引用得到的是引用所指向变量的大小。
  6. 参数传递:当将指针作为参数传递时,实际上是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量。在函数中改变指针的指向不影响实参。而引用作为参数传递时,修改引用会影响实参
  7. 内存占用:引用本质上是一个指针,同样会占用4字节内存;指针是具体变量,需要占用存储空间,具体情况要具体分析。
#include <iostream>

void add(int* p, int& r, int num) {
    *p += num;
    r += num;
}

int main() {
    int x = 10;
    int* p = &x;
    int& r = x;

    std::cout << "Value of x: " << x << std::endl;
    std::cout << "Value pointed by p: " << *p << std::endl;
    std::cout << "Value referred by r: " << r << std::endl;

    add(p, r, 5);

    std::cout << "After addition:" << std::endl;
    std::cout << "Value of x: " << x << std::endl;
    std::cout << "Value pointed by p: " << *p << std::endl;
    std::cout << "Value referred by r: " << r << std::endl;

    // Additional operations to highlight differences
    std::cout << "Address of x: " << &x << std::endl;
    std::cout << "Address stored in p: " << p << std::endl;
    std::cout << "Address of r: " << &r << std::endl;

    // Sizeof operator usage
    double nn = 0;
    double& inf = nn;
    double* ptr = &nn;

    std::cout << "Size of nn: " << sizeof(nn) << std::endl;
    std::cout << "Size of inf: " << sizeof(inf) << std::endl;
    std::cout << "Size of ptr: " << sizeof(ptr) << std::endl;

    return 0;
}
//Value of x : 10
//Value pointed by p : 10
//Value referred by r : 10
//After addition :
//Value of x : 20
//Value pointed by p : 20
//Value referred by r : 20
//Address of x : 000000A26F5FF4A4
//Address stored in p : 000000A26F5FF4A4
//Address of r : 000000A26F5FF4A4
//Size of nn : 8
//Size of inf : 8
//Size of ptr : 8

4 描述一下野指针和悬空指针的概念?

int main(void) {
	int* P = nullptr;
	int* p2 = new int;
	P = p2;
	delete p2;
	p2 = nullptr; 
}

野指针,未被初始化的指针

因此,为了防止出错,对于指针初始化时都是赋值为 nullptr ,这样在使用时编译器就会直接报错,产生非法内存访问。

悬空指针,指针最初指向的内存已经被释放了的一种指针

此时p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为 p=p2=nullptr 。此时再使用,编译器会直接保错。

避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免 悬空指针的产生。

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空

#include <iostream>
#include <memory>

void processResource(int* ptr) {
    if (ptr != nullptr) {
        // 对指针进行操作
        std::cout << "Value at ptr: " << *ptr << std::endl;
    }
    else {
        // 指针为空,无法操作
        std::cout << "ptr is null." << std::endl;
    }
}

int main() {
    int* p;  // 未初始化的指针,可能成为野指针

    // 对野指针进行操作,结果不可预测
    processResource(p);

    int* q = new int(42);  // 动态分配内存并初始化指针

    // 使用指针后释放内存,但没有将指针置空
    delete q;

    // 继续使用悬空指针,结果不可预测
    processResource(q);

    // 使用智能指针std::unique_ptr避免野指针和悬空指针问题
    std::unique_ptr<int> smartPtr(new int(42));

    // 使用智能指针,不再需要手动释放内存
    // 智能指针会在离开作用域时自动释放资源,并将指针置空
    smartPtr.reset();

    // 使用智能指针,避免了悬空指针问题
    processResource(smartPtr.get());

    return 0;
}

5 使用智能指针管理内存资源,RAII是怎么回事?

RAII(资源获取即初始化)是一种C++编程范式,通过在对象的构造函数中获取资源,在析构函数中释放资源,从而实现资源的自动管理。它的核心思想是利用了C++的对象生命周期管理机制,确保在对象创建时自动获取资源,并在对象销毁时自动释放资源,避免了资源泄漏和内存泄漏的问题。

智能指针(如std::shared_ptr和std::unique_ptr)是一种实现了RAII机制的重要工具。它们通过使用对象来管理资源,使得资源的释放不再依赖于开发者手动调用delete或free等函数,而是通过智能指针对象的析构函数自动释放资源。这大大简化了资源管理的代码,并减少了出错的可能性,提高了代码的安全性和可维护性。

因此,使用智能指针可以确保资源的正确释放,避免了手动管理资源带来的问题,提高了代码的健壮性和可靠性。RAII与智能指针的结合是C++编程中的重要实践,帮助开发者更好地管理和使用资源。

6 智能指针的循环引用


#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    std::cout << "a.use_count(): " << a.use_count() << std::endl;
    std::cout << "b.use_count(): " << b.use_count() << std::endl;

    return 0;
}


std::shared_ptr<A> a = std::make_shared<A>(); 
将创建一个新的 A 类对象,并将其包装在一个 std::shared_ptr 中,
然后将这个指针赋值给 a。


在上面的示例中,类A和类B相互持有对方的shared_ptr,形成了循环引用。当main函数结束时,a和b的引用计数都不会变为0,导致它们所管理的对象无法被释放。这就是智能指针循环引用导致的内存泄漏问题。

对象a的引用计数由a自身的std::shared_ptr和b的a_ptr共同维护。

初始时,a的引用计数为1(a自身的std::shared_ptr)。

当b的a_ptr指向a时,a的引用计数也增加1。

当程序离开作用域时,a和b的std::shared_ptr会被销毁,引用计数减1。

然而,由于循环引用的存在,a和b的引用计数都无法减为0,无法触发对象的析构函数。

结果就是两个对象的内存无法被正确释放,造成内存泄漏。

为了解决智能指针循环引用的问题,可以使用std::weak_ptr来打破循环引用。weak_ptr是一种弱引用,不会增加对象的引用计数,可以用来解决循环引用导致的内存泄漏。可以将类A和类B中的某一个指针使用weak_ptr来表示弱引用,从而打破循环引用。

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::weak_ptr<B> b_ptr;  // 修改为 weak_ptr
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // 修改为 weak_ptr
    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;

    std::cout << "a.use_count(): " << a.use_count() << std::endl;
    std::cout << "b.use_count(): " << b.use_count() << std::endl;

    return 0;
}

在上述代码中,类A和类B分别包含了一个std::weak_ptr成员变量b_ptr和a_ptr,

用于持有对方的弱引用。std::weak_ptr是一个非拥有(non-owning)的智能指针,它可以观测(track)一个std::shared_ptr,但不会增加引用计数。

因此,当A和B对象的引用计数归零时,它们的析构函数会被调用,从而正确释放内存。

在main()函数中,我们创建了std::shared_ptr类型的a和b对象,并将它们互相赋值给对方的std::weak_ptr成员变量。由于std::weak_ptr不会增加引用计数,因此不会形成循环引用。在最后打印a.use_count()和b.use_count()可以看到引用计数都为1,没有产生循环引用的问题。

7 左值和右值

#include <iostream>

int main() {
    int x = 10;  // 'x' 是一个左值

    // 'x' 是一个左值,因为它是一个具名变量,可以被赋予新的值
    int& lvalueRef = x;  // 左值引用
    std::cout << "lvalueRef 的值: " << lvalueRef << std::endl;

    // 20 是一个右值,因为它是一个临时值,在表达式结束后不再存在
    int&& rvalueRef = 20;  // 右值引用
    std::cout << "rvalueRef 的值: " << rvalueRef << std::endl;

    return 0;
}

在上面的代码中,x 是一个左值,因为它是一个具名变量,在表达式结束后仍然存在。我们可以使用 & 运算符获取 x 的地址,并将其赋值给一个左值引用 lvalueRef。lvalueRef 的值将与 x 的值相同。

另一方面,20 是一个右值,因为它是一个临时值,在表达式结束后不再存在。我们不能获取 20 的地址。我们可以将其赋值给一个右值引用 rvalueRef,rvalueRef 的值将与 20 相同。

请注意,在 C++ 中,使用 auto 关键字和右值引用 (&&) 可以使代码更灵活简洁地处理左值和右值。

两者的区别

左值: 在 C++ 中,左值指的是可以位于赋值运算符左侧的表达式,或者具有持久性和地址的表达式。通常,变量、对象、数组元素以及返回引用的函数调用都被视为左值。左值具有标识符、地址和在内存中的实际存储。

右值: 右值是指不能位于赋值运算符左侧的表达式,它们通常是临时生成的值,不具有持久性。常见的右值包括字面量、临时变量和返回非引用的函数调用结果。

左值引用: 左值引用是指对左值的引用,使用 & 符号声明。它允许我们创建一个别名来修改或读取已命名的对象。左值引用不能绑定到右值,因为右值不具有持久性。

右值引用: 右值引用是指对右值的引用,使用 && 符号声明。右值引用引入了一种新的引用类别,它可以绑定到右值,但不能绑定到左值。右值引用通常用于实现移动语义和完美转发。

std::move() 函数和右值引用: std::move() 是一个函数模板,用于将一个左值转换为右值引用。它告诉编译器你有意将资源所有权从一个对象转移到另一个对象,以便在资源管理类似于智能指针的情况下避免资源的不必要复制。结合右值引用,std::move() 允许你在合适的情况下实现移动语义,提高性能。

8 右值引用和移动构造

右值引用(Rvalue reference)是一种引用类型,用于绑定到右值表达式,通过 && 表示。它主要用于实现移动语义和完美转发。

移动构造(Move constructor)是一种特殊的构造函数, 用于将右值引用绑定的对象的资源所有权从源对象转移到目标对象,避免不必要的资源拷贝,提高性能。

#include <iostream>

// 定义一个简单的类,包含资源指针和长度信息
class MyArray {
private:
    int* data;
    int length;

public:
    // 构造函数,用于初始化对象
    MyArray(int len) : length(len) {
        data = new int[length];
        std::cout << "调用构造函数,分配长度为 " << length << " 的数组内存" << std::endl;
    }

    // 移动构造函数,用于将资源从源对象移动到目标对象
    MyArray(MyArray&& other) : data(other.data), length(other.length) {
        other.data = nullptr;  // 将源对象的指针设为 nullptr,避免释放资源
        std::cout << "调用移动构造函数,从源对象移动资源" << std::endl;
    }

    // 析构函数,释放资源
    ~MyArray() {
        delete[] data;
        std::cout << "调用析构函数,释放数组内存" << std::endl;
    }
};

int main() {
    // 创建一个临时对象,称为右值
    MyArray tempArray(5);

    // 使用移动构造函数,将右值的资源移动到新的对象
    MyArray newArray(std::move(tempArray));

    return 0;
}

#include <iostream>
#include <vector>
#include <chrono>

class Particle {
public:
    Particle() {
        // 模拟粒子对象的初始化
    }

    // 移动构造函数,通过右值引用实现资源的转移
    Particle(Particle&& other) noexcept {
        // 实现资源的转移,例如拷贝位置和颜色等属性
    }

    // 省略拷贝构造函数和其他函数
};

class ParticleSystem {
public:
    // 添加一个粒子到粒子系统
    void addParticle(Particle&& particle) {
        // 1 函数体内部 particle 被视为左值
        // 2 为了触发移动构造函数,进行显示转换
        particles.push_back(std::move(particle));  // 使用右值引用进行转移
    }

private:
    std::vector<Particle> particles;
};

int main() {
    ParticleSystem system;

    auto startTime = std::chrono::high_resolution_clock::now();

    // 创建大量的粒子并添加到粒子系统
    for (int i = 0; i < 1000000; ++i) {
        Particle p;
        // 1 使用右值引用进行转移,可以将资源从一个对象移动到另一个对象,而不是进行资源的复制。
        // 2 移动构造函数通常比拷贝构造函数更高效,因为它只涉及资源的转移而不是复制。
        system.addParticle(std::move(p));  // 使用右值引用进行转移
    }

    auto endTime = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);

    std::cout << "Time taken: " << duration.count() << " milliseconds" << std::endl;

    return 0;
}

9 无效引用

#include <iostream>

int main() {
    int* ptr = nullptr;  // 将指针初始化为 nullptr

    int& ref = *ptr;  // 声明一个引用并将其绑定到无效的内存位置

    // 尝试使用无效引用
    std::cout << ref << std::endl;  // 这会导致未定义的行为

    return 0;
}

在上面的代码中,我们首先将一个指针 ptr 初始化为 nullptr,表示它不指向任何有效的内存位置。然后,我们声明一个引用 ref 并将其绑定到 *ptr,即无效的内存位置。

接下来,我们尝试使用无效引用 ref 打印其值。由于引用并没有与有效的对象或内存位置绑定,这样的操作会导致未定义的行为。在本例中,程序可能会崩溃、输出奇怪的结果或产生其他无法预测的行为。

因此,避免使用无效引用是非常重要的,我们应该始终确保引用在声明后被正确初始化,并与有效的对象或内存位置绑定,以避免潜在的错误和未定义行为。

10 什么是引用折叠?

引用折叠(Reference collapsing)是一种在 C++ 中处理引用类型组合时的规则。

在 C++ 中,引用类型可以分为左值引用(lvalue reference)和右值引用(rvalue reference)。当将引用类型组合在一起时,引用折叠规则决定了最终的引用类型。

引用折叠规则如下:

  • 当一个左值引用(lvalue reference)与另一个引用(不论是左值引用还是右值引用)组合时,结果仍为左值引用。

例如,T& &(左值引用与引用)折叠为 T&(左值引用)

  • 当一个左值引用(lvalue reference)与一个右值引用(rvalue reference)组合时,结果为右值引用。

例如,T& &&(左值引用与右值引用)折叠为 T&&(右值引用)。

  • 当一个右值引用(rvalue reference)与任何类型的引用组合时,结果仍为右值引用。

例如,T&& &(右值引用与引用)折叠为 T&&(右值引用)。

引用折叠的主要应用在于在模板中处理引用类型参数,特别是与完美转发(perfect forwarding)相关的情况。它确保在模板函数中正确地传递参数的引用类型,以保持参数的值类别(lvalue 或 rvalue)不变。

以下是一些示例,展示引用折叠的应用:

template <typename T>
void func(T&& arg) {
    // 根据引用折叠规则,arg 的类型会根据传入的参数类型进行折叠
}

int main() {
    int x = 10;
    const int& y = x;  // 左值引用
    func(x);  // T 为 int&,arg 为 int& &&,折叠为 int&

    func(5);  // T 为 int,arg 为 int&&,折叠为 int&&

    func(y);  // T 为 const int&,arg 为 const int& &&,折叠为 const int&
    
    return 0;
}

完美转发?

完美转发(Perfect forwarding)是指在函数模板中将参数按原样转发给另一个函数,并保持参数的值类别(左值或右值)不变。它通过引入右值引用的引用折叠规则来实现。

完美转发的主要目的是在保持参数值类别的同时,将函数调用的负担最小化,避免不必要的拷贝或移动操作。它通常与模板和函数重载结合使用,以处理不同类型和值类别的参数。

#include <iostream>
#include <utility>

// 另一个函数,接受参数的值类别
void anotherFunction(int& value) {
    std::cout << "Lvalue reference: " << value << std::endl;
}

void anotherFunction(int&& value) {
    std::cout << "Rvalue reference: " << value << std::endl;
}

// 函数模板,实现完美转发
template <typename T>
void forwardFunction(T&& arg) {
    anotherFunction(std::forward<T>(arg));
}



int main() {
    int x = 10;

    forwardFunction(x);  // 传递左值
    forwardFunction(20); // 传递右值

    return 0;
}

在forwardFunction中,T&& arg是一个通用引用(universal reference),它可以绑定到任意类型的参数,无论是左值还是右值。std::forward是一个转发函数模板,它根据T的值类别来决定如何转发参数。

当forwardFunction被调用时,根据传递的参数的值类别,T会被推导为对应的引用类型。如果传递的是左值,T会被推导为左值引用类型T&;如果传递的是右值,T会被推导为右值引用类型T&&。

11 指针数组和数组指针

指针数组: 指针数组是一个数组,且数组中的元素都是指针类型。 每个元素指向一个独立的对象。可以通过索引访问每个指针元素,进而操作对应的对象。以下是一个指针数组的示例:

int* ptrArray[5];  // 声明一个包含5个指针元素的指针数组

int main() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;

    ptrArray[0] = &a;  // 指针数组的第一个元素指向变量a
    ptrArray[1] = &b;
    ptrArray[2] = &c;
    ptrArray[3] = &d;
    ptrArray[4] = &e;

    for (int i = 0; i < 5; i++) {
        std::cout << *ptrArray[i] << " ";  // 输出每个指针元素所指向的值
    }
    std::cout << std::endl;

    return 0;
}

数组指针: 数组指针是指针,是指指向数组的指针,它指向整个数组而不是数组的单个元素。可以通过指针的偏移来访问数组中的不同元素。以下是一个数组指针的示例:

int arr[5] = {1, 2, 3, 4, 5};  // 声明一个包含5个整数的数组

int (*ptr)[5];  // 声明一个指向包含5个整数的数组的指针

int main() {
    ptr = &arr;  // 数组指针指向数组的首地址

    for (int i = 0; i < 5; i++) {
        std::cout << (*ptr)[i] << " ";  // 通过指针和偏移来访问数组元素
    }
    std::cout << std::endl;

    return 0;
}

12 指针常量和常量指针

指针常量(pointer to constant):

指针常量是指指针本身是一个常量,即指针的值不可修改,但可以通过该指针间接地修改所指向的对象。以下是一个指针常量的示例:

int value = 10;
int* const ptr = &value;  // 声明一个指针常量,指向整数value

*ptr = 20;  // 通过指针间接修改value的值

// ptr = nullptr;  // 错误,无法修改指针的值

常量指针(constant pointer):

常量指针是指指针指向的对象是一个常量,即所指向的对象的值不可修改,但可以修改指针本身。以下是一个常量指针的示例:

int value = 10;
const int* ptr = &value;  // 声明一个常量指针,指向整数value

// *ptr = 20;  // 错误,无法通过常量指针修改value的值

ptr = nullptr;  // 可以修改指针的值

#23届找工作求助阵地##秋招#
C++ 校招面试精解 文章被收录于专栏

适用于 1.C++基础薄弱者 2.想快速上手C++者 3.秋招C++方向查漏补缺者 4.秋招C++短期冲刺者

全部评论
这个引用折叠规则是不是写错了?
点赞 回复 分享
发布于 2023-07-10 14:56 北京
12指针常量 和常量指针英文名是不是反了?
点赞 回复 分享
发布于 2023-08-09 15:39 陕西

相关推荐

点赞 评论 收藏
分享
评论
6
22
分享
牛客网
牛客企业服务