网易互娱 C++开发实习 一面(暑期)

1. 内存对齐怎么理解?下面两个结构体的大小是多少?

答案:内存对齐主要是为了让 CPU 更高效地访问数据。结构体成员不是简单地按字段大小相加,而是会按照成员自身对齐要求和结构体整体对齐要求插入 padding。

struct S1 {
    char c;
    double b;
    int i;
};

struct S2 {
    S1 s;
    float f;
};

在常见 64 位环境下,double 按 8 字节对齐,int 按 4 字节对齐,char 按 1 字节对齐。S1 的布局一般是:

  • char c 占 1 字节
  • 为了让 double b 按 8 字节对齐,中间补 7 字节
  • double b 占 8 字节
  • int i 占 4 字节
  • 结构体整体要按最大对齐值 8 对齐,末尾再补 4 字节

所以 sizeof(S1) 通常是 24。S2S1 占 24 字节,float f 占 4 字节,结构体整体仍然要按 8 字节对齐,所以末尾补 4 字节,sizeof(S2) 通常是 32。

代码:

#include <iostream>
using namespace std;

struct S1 {
    char c;
    double b;
    int i;
};

struct S2 {
    S1 s;
    float f;
};

int main() {
    cout << sizeof(S1) << endl;
    cout << sizeof(S2) << endl;
    return 0;
}

2. 如何减少结构体 S1 的大小?除了改变变量顺序还有什么办法?

答案:减少结构体大小最直接的办法是调整成员顺序,让大对齐成员放前面,小对齐成员放后面,尽量减少中间 padding。

比如把 double 放前面:

struct S1 {
    double b;
    int i;
    char c;
};

这样常见 64 位环境下大小会从 24 降到 16。除了调整顺序,还可以用 #pragma packalignas 控制对齐,但这类方式要慎重。强行压缩对齐可能会导致非对齐访问,某些平台上性能下降,甚至出现访问异常。如果是网络协议或磁盘文件格式,更推荐明确序列化字段,而不是直接把结构体内存写出去。

代码:

#include <iostream>
using namespace std;

struct S1_old {
    char c;
    double b;
    int i;
};

struct S1_new {
    double b;
    int i;
    char c;
};

int main() {
    cout << sizeof(S1_old) << endl;
    cout << sizeof(S1_new) << endl;
    return 0;
}

3. 左值和右值有什么区别?

答案:左值通常表示有稳定身份、可以取地址的对象,比如变量、数组元素、解引用后的指针。右值通常表示临时值,表达式结束后就会消亡,比如字面量、临时对象、函数返回的非引用对象。简单说,左值更强调“对象在哪里”,右值更强调“这个值是什么”。C++11 引入右值引用之后,右值的价值变得更明显,因为它可以用于移动语义,避免不必要的深拷贝。

代码:

#include <iostream>
using namespace std;

int getValue() {
    return 10;
}

int main() {
    int a = 1;      // a 是左值
    int b = getValue(); // getValue() 返回值是右值

    int* p = &a;    // 可以取地址
    // int* q = &getValue(); // 错误,临时右值不能这样取地址

    cout << *p << " " << b << endl;
}

4. 右值引用的作用是什么?

答案:右值引用主要服务于移动语义和完美转发。没有右值引用时,很多临时对象只能被拷贝;有了右值引用后,可以把临时对象内部资源直接转移给新对象,减少内存分配和数据复制。比如一个对象内部维护堆内存,拷贝时要重新分配并复制内容,而移动时只需要转移指针,再把原对象置为空即可。所以右值引用不是为了让语法更复杂,而是为了解决资源转移效率问题。

代码:

#include <iostream>
using namespace std;

class Buffer {
public:
    Buffer(size_t n) : data_(new int[n]), size_(n) {}

    ~Buffer() {
        delete[] data_;
    }

    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

private:
    int* data_;
    size_t size_;
};

5. std::move 的底层原理是什么?

答案:std::move 本身并不会移动任何东西,它只是一个强制类型转换,把表达式转换成右值引用。真正发生资源转移的是后续调用的移动构造函数或移动赋值函数。所以如果一个类没有实现移动构造,或者移动构造不可用,写了 std::move 也不一定能减少拷贝。另外,被 std::move 之后的对象仍然是有效对象,但它的具体内容通常不应该再依赖,只适合析构、重新赋值或者保持可析构状态。

代码:

#include <iostream>
#include <utility>
using namespace std;

class A {
public:
    A() = default;

    A(const A&) {
        cout << "copy\n";
    }

    A(A&&) noexcept {
        cout << "move\n";
    }
};

int main() {
    A a;
    A b = std::move(a);
    return 0;
}

6. 智能指针是什么?为什么要使用智能指针?

答案:智能指针是用对象来管理指针资源的一种 RAII 封装。它的核心作用是把资源释放放到析构函数里,让对象生命周期结束时自动释放资源,减少内存泄漏、重复释放和异常路径忘记释放的问题。常见智能指针有 unique_ptrshared_ptrweak_ptrunique_ptr 表示独占所有权,shared_ptr 表示共享所有权,weak_ptr 用来观察对象并解决循环引用。

代码:

#include <iostream>
#include <memory>
using namespace std;

struct Node {
    ~Node() {
        cout << "~Node\n";
    }
};

int main() {
    unique_ptr<Node> p1 = make_unique<Node>();

    auto p2 = make_shared<Node>();
    weak_ptr<Node> wp = p2;

    if (auto sp = wp.lock()) {
        cout 

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务