[C++常见面试笔试题汇总] 程序设计基础 - 内存分配sizeof指针篇

Posted linuxandmcu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++常见面试笔试题汇总] 程序设计基础 - 内存分配sizeof指针篇相关的知识,希望对你有一定的参考价值。

2 内存分配

系统蓝屏,很大原因都是系统自身代码有缺陷引起的,而系统代码缺陷很大程度上与内存分配不当有关。由于内存分配不当引起的堆栈溢出、缓冲区溢出等问题,常常会导致系统瘫痪甚至崩溃,所以理解内存分配对于一名合格的程序员而言非常有必要。

2.1 内存分配的形式有哪些?

一个C/C++编译的程序所占用的系统内存一般分为以下几个部分的内容:

(1) 由符号起始的区块(Block Started by Symbol, BSS)段:BSS段通常是指用来存放程序中未初始化的全局数据和静态数据的一块内存区域。BSS段属于静态内存分配,程序结束后静态变量资源由系统自动释放。

(2) 数据段(data segment):数据段通常是指用来存放程序中己初始化的全局变量的一块内存区域。数据段也属于静态内存分配。

(3) 代码段(code segmeni/text segment):代码段有时候也叫文本段,通常是指用来存放程序执行代码(包括类成员函数和全局函数以及其他函数代码)的一块内存区域,这部分区域的大小在程序运行前就已经确定,并且内存区域通常是只读。在代码段中,也有可能包含一些只读的常数变量,如字符串常量。

(4) 堆(heap):堆用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc或new等函数分配内存时,新分配的内存就被动态添加到 堆上(堆被扩张),当利用free或delete等函数释放内存时,被释放的内存从堆中被删除(堆被缩减)。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。需要注意的是,它与数据结构中的堆是两回事,分配方式类似于链表。

(5) 栈(stack):栈用户存放程序临时创建的局部变量,一般包括函数括弧中定义的变量。除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等到调用结束后,函数的返回值也会被存放回栈中。栈由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。栈内存分配运算内置于处理器的指令集中,一般使用寄存器来存取,效率很高,但是分配的内存容量有限。

需要注意的是,代码段和数据段之间有明确的分隔,但是数据段和堆栈段之间没有,而且栈是向下增长的,堆是向上增长的。程序示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int global=0; //全局初始化区(数据段)
char *pl; //全局未初始化区(BSS段)

int main()
{
    int a; //栈
    char s[]="abcdefg"; //栈
    char *p2; //栈
    char *p3="123456789"; //p3在栈上,"123456789"在常量区
    static int c=0; //全局(静态)初始化区
    pl=(char *)malloc(100); //分配而来的100B的区域就在堆中 
    strcpy(p1,"123456789") //"123456789"放在常量区,编译器可能会将它与p3所指向的
                           //"123456789"优化成一个地方
    return 0;
}


2.2 什么是内存泄露?

堆是动态分配内存的,并且可以分配使用很大的内存,使用不好会产生内存泄露。频繁地使用malloc和free会产生内存碎片(类似磁盘碎片)。

所谓内存泄露(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。一般常说的内存泄露是指堆内存的泄露,内存泄露其实并非指内存在物理上的消 失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

应用程序一般使用malloc、calloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应地调用free或delete释放该内存块,否则这块内存就不能被再次使用, 造成内存泄露。

例如,对指针进行重新赋值,程序代码如下:

char *memoryArea = malloc(lO); 
char *newArea = malloc(lO); 
memoryArea = newArea;

对memoryArea的赋值会导致memoryArea之前指向的内容丢失,最终造成内存泄露。如

下程序就因为未能对返回值进行处理,最终导致内存泄露。

char *fun()
{
    return malloc(10);
}

void callfun()
{
    fun();
}

内存泄露往往会导致系统出现CPU资源耗尽的严重后果,所以开发人员在编码过程中要养成良好的编程习惯,用malloc或new分配的内存都应当在适当的时机用free或delete释 放,在对指针赋值前,要确保没有内存位置会变为孤立的,另外要正确处理使用动态分配的函数返回值。


2.3 栈空间的最大值是多少?

在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2MB。而申请堆空间的大 小一般小于2GB。

由于内存的读取速度比硬盘快,当程序遇到大规模数据的频繁存取时,开辟内存空间很有作用。栈的速度快,但是空间小,不灵活。堆是向高地址扩展的数据结构,是不连续的内存区域。 这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址的,而堆的大小受限于计算机系统中有效的虚拟内存,所以堆获得的空间比较灵活,也比较大,但是速度相对慢一些。

一般情况下,可以通过以下两种方法更改栈的大小。

(1) link时用/STACK指定它的大小,或者在.def中使用STACKSIZE指定它的大小。

(2) 使用控制台命令“EDITBIN”更改exe的栈空间大小。

需要注意的是,Linux默认栈空间大小为8MB,通过命令ulimit-s来设置。


2.4 什么是缓冲区溢出?

缓冲区是程序运行的时候机器内存中的一个连续块,它保存了给定类型的数据,随着动态分配变量会出现问题。缓冲区溢出是指当向缓冲区内填充数据位数超过了缓冲区自身的容量限制时,发生的溢出的数据覆盖在合法数据(数据、下一条指令的指针、函数返回地址等)上的情况。最好的情况是程序不允许输入超过缓冲区长度的字符并检查数据长度,由于大多数程序 都会假设数据长度总是与所分配的储存空间相当,进而存在缓冲区溢出安全隐患。

程序示例如下:

#include <unistd.h> 

void Test()
{
    char buff[4]; 
    printf("Some input:"); 
    gets(buff); 
    puts(buff);
}   

该程序的Test()函数中使用了标准的C语言输入函 数getc(),由于它没有执行边界检查,最终会导致 Test()函数存在缓冲区溢出安全漏洞。Test()函数的缓冲区最多只能容纳3个字符和一个空字符,所以超过4个字符就会造成缓冲区溢出。

人为的缓冲区溢出一般是由于攻击者写一个超过缓冲区长度的字符串植入到缓冲区,然后再向一个有限空间的缓冲区中植入超长的字符串,这时可能会出现两个结果:一是过长的字符串覆盖了相邻的存储单元,引起程序运行失败,严重的可导致系统崩溃;另一个结果就是利用这种漏洞可以执行任意指令,甚至可以取得系统root特级权限,进而危害系统安全。


3 sizeof

对于变量而言,sizeof的大小就像变量的体积一样,它的大小直接影响着变量的存储与访问效率。

3.1 sizeof是关键字吗?

sizeof是关键字,而非函数,很多时候它都可能被误解是操作符,这是不对的。

引申:预处理指令是否是C语言中的语言类型?

不是。语句是编程语言的基础,C语言中的语言类型一共有以下5种:

(1) 表达式语句。

(2) 函数调用语句。

(3) 控制语句。if语句、switch语句(这两种为条件判断语句)、do while语句、while语 句、for语句(这3种为循环执行语句)、break语句、continue语句、return语句、goto语句 (这4种为转向语句)。

(4) 复合语句。

(5) 空语句。

需要注意的是,由于预处理指令的结尾不能添加分号,所以预处理指令不是语句。


3.2 strlen("")=? sizeof("")=?

strlen("”)= 0,sizeof ("") = 2。

注意""是一个字符串,所以""本质上是一个字符‘‘加上一个结束符‘‘,有2个字节。

具体而言,strlen与sizeof的差别表现在以下几个方面:

(1) sizeof是关键字,而strlen是函数。sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。

(2) sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类 型保证能够容纳实现所建立的最大对象的字节大小。

(3) sizeof可以用类型作为参数,strlen只能用char *做参数,而且必须是以”"结尾的。

看下面的示例程序:

#include <stdio.h>
#include <string.h>

int main()
{
    char * parr = new char[10]; 
    printf("%d
", strlen(parr)); 
    printf("%d
", sizeof(parr)); 
    printf("%d
", sizeof(*parr)); 
    
    return 0;
}

程序输出结果:

14
4
1

在上例中,程序定义了一个字符指针parr,它指向一个分配了10个空间的字符数组,由 于没有进行初始化,根据strlen的计算原理,所以不能够确定strlen(parr)的值,因为无法确定字符串的终止位置,所以该值为一个随机值,本例中输出为14。


3.3 sizeof结构体返回的值?

因为结构体有时候需要字节对齐。一般而言,struct的sizeof是所有成员字节对齐后长度相加,而union的sizeof是取最大的成员长度。

在默认情况下,编译器为每一个变量或数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变默认的对界条件:

(1) 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。

(2) 使用伪指令#pragmapack(),取消自定义字节对齐方式。

字节对齐的细节和编译器实现相关,但一般而言,满足以下3个准则:

(1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

(2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。

(3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

需要注意的是,基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型。如果一个结构体中包含另外一个结构体成员,那么此时最宽基本类型成员不是该结构体成员,而是取基本类型的最宽值。

具体示例程序参考本人另一篇博客: https://www.cnblogs.com/linuxAndMcu/p/9864498.html


3.4 指针强制类型转换后进行加法运算?

假设在32位机器上,由于32位机器上是4字节对齐,以如下结构体为例:

BB 
{
    long num; 
    char * name; 
    short int data; 
    char ha;?
    short ba[5];
}*p;

当 p=0x1000000;则 p+0x200=? (ulong)p+0x200=? (char* )p+0x200=?

其实,在 32 位机器 上,sizeof(struct BBB)=sizeof(*p)=4+4+2+1+1/补齐/+2*5+2/补齐 /=24字节,而p=0x1000000,那么p+0x200=0x1000000+0x20024指针加法,加出来的是指针类型的字节长度的整倍数。就是p偏移sizeof(p)*0x200。

(ulong)p+0x200=0x1000000+0x200经过ulong后,这已经不再是指针加法,而变成一个数值加法了。

(char*)p+0x200=0x1000000+0x200*sizeof(char),结果类型是char*。


3.5 size_t的含义?

size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long unsigned int。

在C++中,设计 size_t 就是为了适应多个平台的 。size_t的引入增强了程序在不同平台上的可移植性。size_t是针对系统定制的一种数据类型,一般是整型。经测试发现,在32位系统中size_t是4字节的,而在64位系统中,size_t是8字节的,这样利用该类型可以增强程序的可移植性。

通常我们用sizeof(XXX)操作,这个操作所得到的结果就是size_t类型。类似的还有wchar_t, ptrdiff_t。wchar_t就是wide char type,“一种用来记录一个宽字符的数据类型”。ptrdiff_t就是pointer difference type,“一种用来记录两个指针之间的距离的数据类型”。


4 指针

用指针变量可以表示各种数据结构,能很方便地使用数组、字符串和链表,并能像汇编语言一样处理内存地址,从而编写出精练而高效的程序。但是由于指针并不是直接操作数据,而 且它可以直接与内存打交道,使用稍有不慎,就会造成严重的后果—程序崩溃,所以在使用指针时一定要深刻理解与指针相关的一些问题。

4.1 使用指针有哪些好处?

指针与其他类型变量一样,不同之处在于一般的变量包含的是实际的真实数据,而指针包含的是一个指向内存中某个位置的地址。指针好处众多,一般而言,使用指针有以下几个方面 的好处:

(1) 可以动态分配内存。

(2) 进行多个相似变量的一般访问。

(3) 为动态数据结构,尤其是树和链表,提供支持。

(4) 遍历数组,如解析字符串。

(5) 高效地按引用“复制”数组与结构,特别是作为函数参数的时候,可以按照引用传递函数参数,提高开发效率。


4.2 引用和指针的区别?

(1) 引用只能在定义时被初始化一次,之后不能被改变,即引用具有“从一而终”的特性。而指针却是可变的。

(2) 引用使用时不需要解引用(*),而指针需要解引用。

(3) 引用不可以为空,而指针可以为空。

(4) 对引用进行sizeof操作得到的是所指向的变量(对象)的大小,而对指针进行sizeof操作得到的是指针本身(所指向的变量或对象的地址)的大小。

(5) 作为参数传递时,两者不同。引用传递参数是“引用传递”,会通过一个间接寻址的方式操作到主调函数中的相关变量。指针传递参数本质上是值传递的方式,它所传递的是一个地址值,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指针引用。


4.3 指针和数组是否表示同一概念

从原理与定义上看,虽然指针与数组表示的是不同的概念,但指针却可以方便地访问数组或者模拟数组,两者存在着一种貌似等价的关系,但也存在着诸多不同之处, 主要表现在以下两个方面:

(1) 修改内容不同。

例如,char a[] = “hello”,可以通过取下标的方式对其元素值进行修改。例如,a[0] = ‘X’ 是正确的,而对于char *p = “world”,此时p指向常量字符串,所以p[0] = ‘X’是不允许的, 编译会报错。

(2) 所占字节数不同。

例如,char *p = “world”,p为指针,则sizeof(p)得到的是一个指针变量的字节数,而不是p所指的内存容量。C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时标记出来。


4.4 野指针?空指针?

指针是指指向不可用内存的指针。

  • 任何指针变量在被创建时,不会自动成为NULL指针(空指针),其默认值是随机的,所以指针变量在创建的同时应当被初始化,或者将指针设置为NULL,或者让它指向合法的内存,而不应该放之不理,否则就会成为野指针。
  • 而同时由于指针被释放(free或delete)后,未能将其设置为NULL,也会导致该指针变为野指针。虽然free和delete把指针所指的内存给释放掉了,但它们并没有把指针本身释放掉,一般可以采用语句if (p!= NULL)进行防错处理,但是if语句却起不到防错作用,因为即使p不是NULL 指针,它也不指向合法的内存块。
  • 第三种造成野指针的原因是指针操作超越了变量的作用范围。

空指针是一个特殊的指针,也是唯一一个对任何指针类型都合法的指针。指针变量具有空指针值,表示它当时处于闲置状态,没有指向有意义的内容。为了提高程序的可读性,标准库定义了一个与0等价的符号常量NULL,程序里可以写p = 0或者p = NULL,两种写法都把 p置为空指针值。C语言保证这个值不会是任何对象的地址。给指针值赋零则使它不再指向任何有意义的东西。

作为一种风格,很多程序员一般不愿意在程序中到处出现未加修饰的0,所以习惯定义预 处理宏NULL (在<stdio.h>和其他几个头文件中)为空指针常数,通常是0或者((void *)0)。希望区别整数0和空指针0的程序员可以在需要空指针的地方使用NULL。(C++11标准中引入了nullptr)

通用指针可以指向任何类型的变量。通用指针的类型用(void*)表示,因此也称为void指针。

以上是关于[C++常见面试笔试题汇总] 程序设计基础 - 内存分配sizeof指针篇的主要内容,如果未能解决你的问题,请参考以下文章

125条常见的java面试笔试题大汇总

java面试笔试题大汇总

java面试笔试题大汇总

2021年最新PHP 面试笔试题汇总

java面试笔试题逻辑题,大厂面试题汇总

MySQL常见面试笔试题