秋招C++八股-内存相关(持续更新)

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



需要1000道面试PDF,【正在跳转】

1 说明C++的内存分区

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限

堆:就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete 。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收

自由存储区:就是那些由 malloc 等分配的内存块,它和堆是十分相似的,不过它是用 free 来结束自己的生命的

全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变 量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定 义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0

常量存储区:这是一块比较特殊的存储区,这里面存放的是常量,不允许修改

代码区:存放函数体的二进制代码

2 C++ 类的数据成员和成员函数的内存分布情况?

C++类是由结构体发展得来的,所以他们的成员变量(C语言的结构体只有成员变量)的内存分配机制 是一样的。下面我们以类来说明问题,如果类的问题通了,结构体也就没问题了。

类分为成员变量和成员函数,我们先来讨论成员变量。

一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。(在定义类对象的同时这些成员变量也就被定义了),举个例子:

#include 
using namespace std;

class MyClass {
public:
    MyClass() : num(0) {}

    void memberFunction() {
        std::cout << "This is a member function." << std::endl;
    }

    static int staticVariable;

public:
    int num;
};

int MyClass::staticVariable = 10;

int main() {
    MyClass obj;
    obj.memberFunction();

    cout << "对象地址:" << &obj << endl;
    cout << "age地址:" << &(obj.num) << endl;
    std::cout << "Size of object: " << sizeof(obj) << std::endl;
    std::cout << "Size of static variable: " << sizeof(MyClass::staticVariable) << std::endl;
    cout << "静态成员变量地址:" << &MyClass::staticVariable << endl;

    return 0;
}
This is a member function.
对象地址:000000A1876FF9A4
age地址:000000A1876FF9A4
Size of object: 4
Size of static variable: 4
静态成员变量地址:00007FF68269E010

成员函数不会直接存储在对象内存中,而是共享一份函数代码,因此它们不会增加对象的大小

  1. 对于普通成员函数,编译器会隐式地为其添加一个额外的参数,即指向当前对象的指针(this指针)。通过this指针,成员函数可以访问对象的数据成员和其他成员函数。因此,成员函数本身并不存储在对象内存中,而是通过指针进行间接访问
  2. 静态成员函数与普通成员函数的存储方式是相同的,它们都存放在代码区。静态成员函数没有隐式的this指针,因此无法直接访问非静态数据成员。静态成员函数的特点是它们独立于任何对象,可以直接通过类名来调用,而不需要创建对象实例。
  3. 关于静态成员变量,它们在类中只有一份拷贝,不属于对象的一部分,而是属于整个类。静态成员变量存储在静态数据区,而不是对象的内存中。

总结起来,类的对象中只存储非静态数据成员,而成员函数和静态成员变量不占用对象的内存空间。这样设计的好处是,不管类有多少个对象实例,成员函数和静态成员变量只有一份拷贝,节省了内存空间

3 什么是内存泄漏应该如何避免?

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用mallocreallocnew等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用freedelete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

避免内存泄露的几种方式:

  1. 计数法:使用new或者malloc时,让该数+1deletefree时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露。
  2. 一定要将基类的析构函数声明为虚函数。对象数组的释放一定要用**delete []**
  3. new就有delete,有malloc就有free,保证它们一定成对出现。

检测工具:

4 堆和栈的区别?

  1. 管理方式不同:由操作系统自动分配和释放,无需手动控制;的申请和释放由程序员负责,容易产生内存泄漏。
  2. 空间大小不同:每个进程拥有的栈大小远小于堆大小。在理论上,进程可申请的堆大小为虚拟内存大小,而栈的大小通常较小,例如64位的Windows默认为1MB,64位的Linux默认为10MB。
  3. 生长方向不同:的生长方向是向上,即内存地址由低到高增长;栈的生长方向是向下,即内存地址由高到低增长。
  4. 分配方式不同:堆是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,例如局部变量的分配;动态分配使用alloca()函数,但是栈的动态分配和堆是不同的,栈的动态分配由操作系统进行释放,无需手动实现。
  5. 分配效率不同:栈由操作系统自动分配,对栈提供了硬件级别的支持,包括专门的寄存器存放栈的地址以及专门的指令来执行压栈和出栈操作,因此栈的分配效率较高。堆则是由C/C++提供的库函数或运算符来完成申请和管理,实现机制较为复杂,频繁的内存申请可能导致内存碎片,因此堆的分配效率较低。

堆适用的场景:

  1. 动态内存需求:堆适用于需要在运行时动态分配内存的情况。当程序需要根据实际情况动态地分配和释放内存时,堆提供了灵活性。
  2. 大内存需求:堆适用于需要较大内存空间的情况。堆的可用内存空间较大,可以满足对大型数据结构、数组或复杂对象的需求。
  3. 长期存储:堆适用于需要长期存储数据的情况。在堆上分配的内存不会随着函数的调用结束而释放,可以在程序的不同部分共享和访问。

栈适用的场景:

  1. 局部变量和临时数据:栈适用于存储局部变量和临时数据。由于栈的分配和释放由操作系统自动处理,适合于函数内部的变量和数据的临时存储。
  2. 快速分配和释放:栈适用于需要频繁分配和释放内存的情况。栈上的内存分配和释放操作非常高效,适合于快速的临时数据操作。
  3. 有限的内存需求:栈适用于内存需求较小的情况。每个进程的栈大小有限,适合存储相对较小的数据结构和变量。

5 new 和 delete 是如何实现的?

1.new的实现过程:

补充:

1、 new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数

对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;

① new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;

② 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;

③ 对象被分配了空间并构造完成,返回一个指向该对象的指针。

2、 delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。

3、 需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了

6 new / delete 与 malloc / free的异同

  1. 功能:new和malloc都可以用于内存的动态申请和释放。
  2. 语言支持:new是C++运算符,malloc是C/C++语言标准库函数,因此它们所属的语言环境不同。
  3. 内存大小计算:new会根据类型自动计算要分配的空间大小,而malloc需要手工计算所需的空间大小,并传递给它作为参数。
  4. 类型安全性:new是类型安全的,会自动调用相关对象的构造函数进行初始化,而malloc不会进行类型检查和初始化
  5. 构造函数和析构函数的调用:new会在内存分配后调用对象的构造函数进行初始化,并在delete时调用对象的析构函数进行清理。而malloc和free没有相关的构造函数和析构函数的调用。
  6. 库文件依赖:malloc和free是C/C++标准库函数,需要链接相应的库文件支持,而new和delete不需要。
  7. 错误处理:使用new分配内存后,如果直接使用delete释放内存,会自动调用对象的析构函数进行清理;而使用free释放new分配的内存,则不会调用对象的析构函数,只是简单地释放内存。
#include 
#include 

class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    // 使用new和delete进行内存的动态申请和释放
    MyClass* obj1 = new MyClass(); // 使用new分配内存,并调用构造函数
    delete obj1; // 使用delete释放内存,并调用析构函数

    std::cout << std::endl;

    // 使用malloc和free进行内存的动态申请和释放
    MyClass* obj2 = static_cast(std::malloc(sizeof(MyClass))); // 使用malloc分配内存
    free(obj2); // 使用free释放内存,但不会调用构造函数和析构函数

    return 0;
}

7 如何用代码判断大小端存储?

// 大端
     0x12        0x34        0x56        0x78
+-----------+-----------+-----------+-----------+
|    0x12   |    0x34   |    0x56   |    0x78   |
+-----------+-----------+-----------+-----------+

// 小端
     0x12        0x34        0x56        0x78
+-----------+-----------+-----------+-----------+
|    0x78   |    0x56   |    0x34   |    0x12   |
+-----------+-----------+-----------+-----------+
#include 
using namespace std;

int main() {
    int a = 0x12345678; // 4
    // 由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a); // 1
    if (c == 0x12)
        cout << "big endian" << endl;
    else if (c == 0x78)
        cout << "little endian" << endl;
}

8 static的用法和作用?

以下是关于static关键字的总结:

1.隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数具有全局可见性

2.持久性:static变量具有记忆功能和全局生存期,存储在静态数据区的变量在程序开始运行时完成初始化,且只初始化一次。

3.默认初始化为0:静态变量在静态数据区存储,并且默认被初始化为0x00,减少程序员的工作量。

4.类成员声明static:

5.类内:

9 静态变量什么时候初始化?

在 C++ 中,静态变量的初始化时机分为两种情况:

  1. 在全局作用域下定义的静态变量会在程序开始执行之前进行初始化。编译器在编译阶段会为全局静态变量分配内存,并将其初始化为指定的初值或默认值。
  2. 在函数内部的静态局部变量的初始化时机是在其定义语句第一次执行时。当程序流程首次经过静态局部变量的定义语句时,编译器会为其分配内存并进行初始化。而在后续的函数调用中,静态局部变量不会重新进行初始化,只是保留上一次赋值后的值。

静态变量的初始化只会发生一次,不会重复进行。静态变量的内存分配在程序运行之前就已经完成,所以它们具有全局的生存期,并在整个程序执行期间保持存在。

#include 

void func() {
    static int staticVar = 42;  // 静态局部变量,在第一次执行时进行初始化
    std::cout << "staticVar: " << staticVar << std::endl;
    staticVar++;  // 可以多次赋值
}

int main() {
    std::cout << "Before calling func()" << std::endl;
    func();  // 第一次执行 func(),静态局部变量进行初始化
    std::cout << "After calling func()" << std::endl;
    func();  // 后续的函数调用,静态局部变量不会重新初始化

    return 0;
}

在这个示例中,函数 func() 内部包含一个静态局部变量 staticVar,在第一次调用 func() 时,它会被初始化为 42,并输出该值。在后续的函数调用中,静态局部变量的值会保留上一次赋值后的结果,并进行自增操作。输出结果将显示出静态局部变量的初始化和后续调用的效果。

10 类中静态变量的初始化是什么时候?

#include 

class MyClass {
public:
    static int staticVar;

    MyClass() {
        std::cout << "Constructor called" << std::endl;
    }
};

int MyClass::staticVar = 42;

int main() {
    std::cout << "Main function start" << std::endl;
    
    std::cout << "Accessing staticVar: " << MyClass::staticVar << std::endl;

    MyClass obj;

    std::cout << "Main function end" << std::endl;

    return 0;
}

在这个示例中,MyClass 类包含一个静态变量 staticVar。它在类的外部进行了初始化,并且在程序开始执行之前进行了静态初始化。然后,在 main 函数中,我们访问了 staticVar 的值,并创建了一个 MyClass 对象。输出结果将显示静态变量的初始值以及构造函数的调用。

11 什么是内存对齐?

内存对齐可以提高CPU的内存访问效率,因为CPU在读取内存时是按照一块一块的方式进行读取,每块的大小由内存读取粒度确定,通常为2、4、8或16个字节。

假设内存读取粒度为4字节,当CPU需要读取一个4字节的数据到寄存器中时,如果数据从0字节开始存储,那么CPU可以直接将0-3四个字节完全读取到寄存器中进行处理。

但如果数据从1字节开始存储,CPU首先需要读取前4个字节数据到寄存器,再次读取4-7个字节数据进入寄存器。然后,CPU将剔除0字节、5字节、6字节和7字节的数据,并最终将1字节、2字节、3字节和4字节的数据合并进入寄存器。可以看到,当内存没有对齐时,CPU需要进行额外的操作,导致性能降低。

另外,还有 个就是,有的 CPU 遇到未进 内存对 的处理直接拒绝处理,不是所有的 硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类 型的数据,否则抛出硬件异常。所以内存对 还有利于平台移植。

使用 #pragma pack(n) 后,对齐规则会变化,以下是变化后的规则:

偏移量要是 n 和当前变量大小中较小值的整数倍。

整体大小要是 n 和最大变量大小中较小值的整数倍。

n 值必须是1、2、4、8等,如果是其他值,则按照默认的分配规则。

这样可以通过指定合适的 n 值来控制结构体的对齐方式,以满足特定的需求。但需要注意,改变对齐方式可能会影响内存访问的效率和可移植性,因此需要谨慎使用,并了解对程序的影响。

#include 

// 结构体定义
struct Data {
    char ch;     // 1字节
    int num;     // 4字节
    double value; // 8字节
};

int main() {
    Data data;

    std::cout << "Size of Data struct: " << sizeof(Data) << " bytes" << std::endl;
    std::cout << "Address of ch: " << reinterpret_cast(&data.ch) << std::endl;
    std::cout << "Address of num: " << reinterpret_cast(&data.num) << std::endl;
    std::cout << "Address of value: " << reinterpret_cast(&data.value) << std::endl;

    return 0;
}

根据内存对齐的原则,结构体的每个成员变量的地址都必须是其类型大小的整数倍。在这个例子中,char类型占用1字节,int类型占用4字节,double类型占用8字节。

虽然char类型只需要1字节,但是为了满足对齐要求,编译器会在char变量后面添加3字节的填充空间,使得int变量的地址为4的倍数。

因此,实际上char变量占用了4字节的空间,其中只有1字节用于存储数据,其余3字节为填充空间。

12 求类/结构体大小

12.1 定义一个struct,有int x,char c两个成员,这个结构体的大小?

#include 

struct MyStruct {
    int x;
    char c;
};

int main() {
    std::cout << "Size of MyStruct: " << sizeof(MyStruct) << " bytes" << std::endl;
    return 0;
}

12.2 如果增加一个static int 这个结构体大小怎么变化?

如果在结构体中增加一个 static int 成员变量,它不会增加结构体的大小。静态成员变量是与类/结构体本身相关联的,而不是与类/结构体的每个实例相关联的。无论创建多少个结构体的实例,静态成员变量只有一个副本。

#include 

struct MyStruct {
    int x;
    char c;
    static int staticInt;
};

int main() {
    std::cout << "Size of MyStruct: " << sizeof(MyStruct) << " bytes" << std::endl;
    return 0;
}

12.3 如果在结构体里定义一个虚函数,结构体大小怎么变化?

如果在结构体(或类)中定义了虚函数,它会增加结构体的大小。这种增加的大小通常以虚函数表(vtable)或虚函数指针的形式存在。

具体的大小增加取决于使用的编译器和平台,以及虚函数的具体实现方式。然而,常见的做法是编译器在结构体中添加一个指针大小的成员,该指针指向虚函数表。虚函数表是一个包含指向结构体虚函数的指针的数据结构。

#include 

struct MyStruct {
    int x;
    char c;

    virtual void foo() {}
};

int main() {
    std::cout << "MyStruct 的大小为:" << sizeof(MyStruct) << " 字节" << std::endl;
    return 0;
}

13 深拷贝和浅拷贝

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原 来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也 不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝 的

#include   
#include 
using namespace std;
class Student
{
private:
	int num;
	char* name;
public:
	Student() {
		name = new char(20);
		cout << "Student" << endl;
	};
	~Student() {
		cout << "~Student " << &name << endl;
		delete name;
		name = NULL;
	};
	Student(const Student& s) {//拷贝构造函数
		//浅拷贝,当对象的name和传入对象的name指向相同的地址
		//name = s.name;
		//深拷贝
		name = new char(20);
		memcpy(name, s.name, strlen(s.name));
		cout << "copy Student" << endl;
	};
};
int main()
{
	{// 花括号让s1和s2变成局部对象,方便测试
		Student s1;
		Student s2(s1);// 复制对象
	}
	system("pause");
	return 0;
}

//浅拷贝执行结果:

//Student

//copy Student

//~Student 0x7fffed0c3ec0

//~Student 0x7fffed0c3ed0

//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop):

//0x0000000001c82c20 * **

//深拷贝执行结果:

//Student

//copy Student

//~Student 0x7fffebca9fb0

//~Student 0x7fffebca9fc0

展开阅读全文

页面更新:2024-03-02

标签:内存   初始化   变量   字节   静态   函数   分配   大小   对象   成员

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top