c++面试突击:第一章:编译内存相关 堆,栈,和内存(中)

本文详细介绍了C++中栈和堆的区别,包括内存分配方式、效率和作用,以及变量的类型和作用域。讨论了如何限制类对象在堆或栈上创建,并解释了内存对齐的原理和好处。最后探讨了类大小的计算规则,涉及虚函数和虚继承的影响。
摘要由CSDN通过智能技术生成

三,栈和堆的区别

面试高频指数:★★★★★

申请方式:栈是系统自动分配,堆是程序员主动申请。
申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。

申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。

**

四,变量的区别

**
面试高频指数:★★★☆☆

全局变量、局部变量、静态全局变量、静态局部变量的区别 C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6
种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。

从作用域看:

全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern关键字再次声明这个全局变量。

静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被
static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:

静态存储区:全局变量,静态局部变量,静态全局变量。 栈:局部变量。 说明:

静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data
段)中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。

五,如何限制类的对象只能在堆上创建?如何限制对象只能在栈上创建?

说明:C++ 中的类的对象的建立分为两种:静态建立、动态建立

静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:A a; 动态建立:使用 new
关键字在堆空间上创建对象,底层首先调用 operator new()
函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A(); 限制对象只能建立在堆上:

最直观的思想:避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行,因为当构造函数设置为私有后,不能在类的外部调用构造函数来构造对象,只能用
new 来建立对象。但是由于 new 创建对象时,底层也会调用类的构造函数,将构造函数设置为私有后,那就无法在类的外部使用 new
创建对象了。因此,这种方法不可行。

解决方法 1:

将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。

C++

class A
{
public:
    A() {}
    void destory()
    {
        delete this;
    }

private:
    ~A()
    {
    }
};

该方法存在的问题:

用 new 创建的对象,通常会使用 delete 释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个
destory() 函数,用来释放 new 创建的对象。

无法解决继承问题,因为如果这个类作为基类,析构函数要设置成
virtual,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。

解决方法 2:

构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为
protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。

C++

class A
{
protected:
    A() {}
    ~A() {}

public:
    static A *create()
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

限制对象只能建立在栈上:

解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用
operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在

堆上。 C++

class A
{
private:
    void *operator new(size_t t) {}    // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
    A() {}
    ~A() {}
};

六,内存对齐

面试高频指数:★★☆☆☆

什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 内存对齐的原则:

结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除; 结构体每个成员相对于结构体首地址的偏移量 (offset)
都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing
padding)。

实例:

C++

/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>

using namespace std;

struct A
{
    short var; // 2 字节
    int var1;  // 8 字节 (内存对齐原则:填充 2 个字节) 2 (short) + 2 (填充) + 4 (int)= 8
    long var2; // 12 字节 8 + 4 (long) = 12
    char var3; // 16 字节 (内存对齐原则:填充 3 个字节)12 + 1 (char) + 3 (填充) = 16
    string s;  // 48 字节 16 + 32 (string) = 48
};

int main()
{
    short var;
    int var1;
    long var2;
    char var3;
    string s;
    A ex1;
    cout << sizeof(var) << endl;  // 2 short
    cout << sizeof(var1) << endl; // 4 int
    cout << sizeof(var2) << endl; // 4 long
    cout << sizeof(var3) << endl; // 1 char
    cout << sizeof(s) << endl;    // 32 string
    cout << sizeof(ex1) << endl;  // 48 struct
    return 0;
}
进行内存对齐的原因:(主要是硬件设备方面的问题)

某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常; 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间; 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment
trap); 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。 内存对齐的优点:

便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。

七,类的大小

面试高频指数:★★☆☆☆

类大小的计算 说明:类的大小是指类的实例化对象的大小,用 sizeof 对类型名操作时,结果是该类型的对象的大小。

计算原则:

遵循结构体的对齐原则。

与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
虚函数对类的大小有影响,是因为虚函数表指针的影响。 虚继承对类的大小有影响,是因为虚基表指针带来的影响。
空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。

实例:
简单情况和空类情况

C++

/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>

using namespace std;

class A
{
private:
    static int s_var; // 不影响类的大小
    const int c_var;  // 4 字节
    int var;          // 8 字节 4 + 4 (int) = 8
    char var1;        // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
    A(int temp) : c_var(temp) {} // 不影响类的大小
    ~A() {}                    // 不影响类的大小
};

class B
{
};
int main()
{
    A ex1(4);
    B ex2;
    cout << sizeof(ex1) << endl; // 12 字节
    cout << sizeof(ex2) << endl; // 1 字节
    return 0;
}

带有虚函数的情况:(注意:虚函数的个数并不影响所占内存的大小,因为类对象的内存中只保存了指向虚函数表的指针。)

C++

/*
说明:程序是在 64 位编译器下测试的
*/

> #include <iostream>
> 
> using namespace std;
> 
> class A { private:
>     static int s_var; // 不影响类的大小
>     const int c_var;  // 4 字节
>     int var;          // 8 字节 4 + 4 (int) = 8
>     char var1;        // 12 字节 8 + 1 (char) + 3 (填充) = 12 public:
>     A(int temp) : c_var(temp) {} // 不影响类的大小
>     ~A() {}                      // 不影响类的大小
>     virtual void f() { cout << "A::f" << endl; }
> 
>     virtual void g() { cout << "A::g" << endl; }
> 
>     virtual void h() { cout << "A::h" << endl; } // 24 字节 12 + 4 (填充) + 8 (指向虚函数的指针) = 24 };
> 
> int main() {
>     A ex1(4);
>     A *p;
>     cout << sizeof(p) << endl;   // 8 字节 注意:指针所占的空间和指针指向的数据类型无关
>     cout << sizeof(ex1) << endl; // 24 字节
>     return 0; }

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值