分享免费的编程资源和教程

网站首页 > 技术教程 正文

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

goqiw 2024-10-04 22:07:30 技术教程 21 ℃ 0 评论

整理全网c++1000道面试题。文章所展现为统计高频题及答案。



需要1000道面试PDF,【「链接」

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 = #

    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时,智能指针会自动释放动态分配的资源。
  2. unique_ptr: unique_ptr采用独享所有权的语义,一个非空的unique_ptr始终拥有它所指向的资源。它不支持普通的拷贝和赋值操作,因此不能用于STL标准容器,除非通过移动语义将所有权转移给另一个unique_ptr。当unique_ptr被销毁或重置时,它会自动释放所拥有的资源。
  3. weak_ptr: weak_ptr是一种弱引用,它用于解决shared_ptr可能形成的循环引用问题。weak_ptr指向由shared_ptr管理的对象,但不增加引用计数。当所有shared_ptr析构后,即使还有weak_ptr引用该对象,内存也会被释放。为了使用weak_ptr,可以使用其成员函数lock()检查是否指向有效的内存。
  4. auto_ptr(已被废弃): auto_ptr是C++98标准中引入的智能指针,主要用于解决异常发生时可能导致内存泄漏的问题。然而,auto_ptr存在一些问题,包括拷贝语义导致源对象变为无效和不能在STL容器中使用。由于这些问题,C++11引入了更安全和功能更强大的unique_ptr来替代auto_ptr。
template<typename T>
class SharedPtr
{
public:
    SharedPtr(T* ptr = NULL) : _ptr(ptr), _pcount(new int(1)) {} // 构造函数,初始化指针和引用计数为1

    SharedPtr(const SharedPtr& s) : _ptr(s._ptr), _pcount(s._pcount) // 拷贝构造函数,共享指针和引用计数
    {
        *(_pcount)++; // 增加引用计数
    }

    SharedPtr<T>& operator=(const SharedPtr& s) // 赋值运算符重载,实现指针和引用计数的赋值
    {
        if (this != &s)
        {
            if (--(*(this->_pcount)) == 0) // 减少当前对象的引用计数
            {
                delete this->_ptr; // 释放当前对象指向的资源
                delete this->_pcount; // 释放当前对象的引用计数
            }
            _ptr = s._ptr; // 将指针和引用计数赋值为源对象的值
            _pcount = s._pcount;
            *(_pcount)++; // 增加引用计数
        }
        return *this;
    }

    T& operator*() // 解引用运算符重载,返回指针指向的对象的引用
    {
        return *(this->_ptr);
    }

    T* operator->() // 成员访问运算符重载,返回指针
    {
        return this->_ptr;
    }

    ~SharedPtr() // 析构函数,负责资源的释放
    {
        --(*(this->_pcount)); // 减少引用计数
        if (this->_pcount == 0) // 当引用计数为0时
        {
            delete _ptr; // 释放指针指向的资源
            _ptr = NULL;
            delete _pcount; // 释放引用计数
            _pcount = NULL;
        }
    }

private:
    T* _ptr; // 指向动态分配对象的指针
    int* _pcount; // 指向引用计数的指针
};

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

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

在构造函数中,引用计数被初始化为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,导致它们所管理的对象无法被释放。这就是智能指针循环引用导致的内存泄漏问题。

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

对象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,无法触发对象的析构函数。

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

#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;
}

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 关键字和右值引用 (&&) 可以使代码更灵活简洁地处理左值和右值。

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;
}

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;
}

11 完美转发?

完美转发(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;
}

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表