3.内存管理&编程题(20道)
3.1由gcc编译的C语言程序占用的内存分为哪几个部分?
栈区(stack) | 存放函数的参数、局部变量。 |
堆区(heap) | 提供程序员动态申请的内存空间。 |
全局(静态)区(static) | 存放全局变量和静态变量,初始化不为0的全局变量和静态变量、const型常量在一块区域(.data段),未初始化的、初始化为0的全局变量和静态变量在相邻的另一块区域(.bss段)。 |
程序代码区 | 存放函数体的二进制代码和字符串常量。 |
如何判读一个系统的大小端存储模式?
(1)方法一:int *强制类型转换为char *,用“[]”解引用
void checkCpuMode(void) { int c = 0x12345678; char *p = (char *)&c; if(p[0] == 0x12) printf("Big endian.\n"); else if(p[0] == 0x78) printf("Little endian.\n"); else printf("Uncertain.\n"); }
(2)方法二:int *强制类型转换为char *,用“*”解引用
void checkCpuMode(void) { int c = 0x12345678; char *p = (char *)&c; if(*p == 0x12) printf("Big endian.\n"); else if(*p == 0x78) printf("Little endian.\n"); else printf("Uncertain.\n"); }
(3)方法三:包含short跟char的共用体
void checkCpuMode(void) { union Data { short a; char b[sizeof(short)]; }data; data.a = 0x1234; if(data.b[0] == 0x12) printf("Big endian.\n"); else if(data.b[0] == 0x34) printf("Little endian.\n"); else printf("uncertain.\n"); }
3.3全局变量和局部变量的区别?
(1)全局变量储存在静态区,进入main函数之前就被创建,生命周期为整个源程序。
(2)局部变量在栈中分配,在函数被调用时才被创建,在函数退出时销毁,生命周期为函数内。
3.4以下程序中,主函数能否成功申请到内存空间?
#include<stdio.h> #include<stdlib.h> #include<string.h> void getmemory(char *p) { p = (char *)malloc(100); strcpy(p, "hello world"); } int main() { char *str = NULL; getmemory(str); printf("%s\n", str); free(str); return 0; }
答案:不能。
解读:getmemory(str)没能改变str的值,因为传递给子函数的只是str的复制值NULL,main函数中的str一直都是 NULL。正确的getmemory()如下:
①传递的是二重指针,即str的指针 void getmemory(char **p) { *p = (char *)malloc(100); strcpy(*p, "hello world"); } ②传递的是指针别名,即str的别名,C++中 void getmemory(char * &p) { p = (char *)malloc(100); strcpy(p, "hello world"); }
void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf("%s\n", str); }
答案:内存泄漏。
解读:调用malloc()申请内存空间,使用完毕之后没有调用free()释放内存空间并使指针指向NULL。
3.6 请问运行下面的Test()函数会有什么样的后果?
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf("%s\n", str); }
答案:打印野指针内容,可能是乱码。
解读:GetMemory()返回的是指向栈内存的指针,但该栈内存已被释放,该指针的地址不是 NULL,成为野指针,新内容不可知。
3.7 请问运行下面的Test()函数会有什么样的后果?
void Test(void) { char *str = (char *) malloc(100); strcpy(str,"hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf("%s\n", str); } }
答案:篡改堆区野指针指向的内容,后果难以预料,非常危险。
解读:
(1)free(str);之后,str成为野指针,没有置为NULL,if(str != NULL)语句不能阻止篡改操作。
(2)野指针不是NULL指针,是指向被释放的或者访问受限的内存的指针。
(3)造成野指针原因:①指针变量没有被初始化,任何刚创建的指针不会自动成为NULL;②指针被free或delete之后,没有置NULL;③指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
3.8在C语言中memcpy和memmove是一样的吗?
答案:
(1)memcpy()与memmove()一样都是用来拷贝src所指向内存内容前n个字节到dest所指的地址上。
(2)不同的是,当src和dest所指的内存区域重叠时,memcpy可能无法正确处理,而memmove()仍然可以正确处理,不过执行效率上略慢些。
解读:
(1)memcpy()无论什么情况下,都是从前往后拷贝内存。当源地址在前,目的地址在后,且两个区域有重叠时,会造成拷贝错误,达不到理想中的效果。
void *memcpy(void *dest, const void *src, size_t count) { if(dest == NULL || src == NULL || count <= 0) return NULL; char *d = (char *)dest; char *s = (char *)src; while(count--) { *d++ = *s++; } return dest; }
(2)memmove()则分两种情况:目的地址在前,源地址在后的情况下,从前往后拷贝内容。否则从后往前拷贝内容。无论什么情况都能达到理想中的效果。
void *memmove(void *dest, const void *src, size_t count) { if(dest == NULL || src == NULL || count <= 0) return NULL; if(dest < src) { char *d = (char *)dest; char *s = (char *)src; while (count--) { *d++ = *s++; } } else { char *d = (char *)dest + count; char *s = (char *)src + count; while (count--) { *--d = *--s; } } return dest; }
(1)malloc函数的底层实现是操作系统有一个由可用内存块连接成的空闲链表。调用malloc时,它将遍历该链表寻找足够大的内存空间,将该块一分为二(一块与用户申请的大小相等,另一块为剩下来的碎片,会返回链表),调用free函数时,内存块重新连接回链表。
3.10在1G内存的计算机中能否通过malloc申请大于1G的内存?为什么?
(1)可以。
(2)因为malloc函数是在程序的虚拟地址空间申请的内存,与物理内存没有直接的关系。虚拟地址与物理地址之间的映射是由操作系统完成的,操作系统可通过虚拟内存技术扩大内存。
3.11内存泄漏是什么?
(1)内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
(2)分类:
①常发性内存泄漏:发生泄漏的代码会被多次执行到。
②偶发性内存泄漏:发生泄漏的代码在某些环境或操作下才会发生。
③一次性内存泄漏:只会被执行一次。
④隐式内存泄漏:程序在运行过程中不停地分配内存,直到结束时才释放。严格来讲这不算内存泄漏,但服务器运行时间很长,可能会耗尽所有内存。
3.12内存溢出是什么?与内存泄漏有何关系?
(1)内存溢出(Out Of Memory)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于系统能提供的最大内存。此时程序无法运行,系统提示内存溢出。有时候会自动关闭软件。
(2)造成内存溢出的原因:
①内存泄漏的堆积最终导致内存溢出。
②需要保存多个耗用内存过大的对象或加载单个超大的对象时,其大小超过了当前剩余的可用内存空间。
3.13堆栈溢出一般是由什么原因导致的?
(1)堆栈溢出一般包括堆内存溢出和栈内存溢出,两者都属于缓冲区溢出。
(2)堆内存溢出可能是堆的尺寸设置得过小/动态申请的内存没有释放。
(3)栈内存溢出可能是栈的尺寸设置得过小/递归层次太深/函数调用层次过深/分配了过大的局部变量。
3.14内存溢出和内存越界的区别?
(1)内存溢出:要求分配的内存超出了系统所能给予的,于是产生溢出。
(2)内存越界:向系统申请了一块内存,而在使用时超出了申请的范围,常见的是数组访问越界。
3.15位翻转
思路:目标数初始化为0,用&0x01的方式获得原始数的第1位,然后左移7位再与目标数按位或,接着原始数右移一位;再用&0x01的方式获得原始数的第2位,然后左移6位……如此循环8次即可。最后返回目标数。
代码:
unsigned char bit_reverse(unsigned char input) { unsigned char result = 0; int bit = 8; while(bit--) { result |= ((input & 0x01) << bit); input >>= 1; } return result; }
思路:双指针法,两个指针,一个指向字符串开头,另一个指向字符串结尾,相互交换指向的内容,接着头指针前进,尾指针后退,交换内容……直到两指针相遇。
代码:
#include<string.h> void inverted_order(char *p) { char *s1, *s2, tem; s1 = p; s2 = s1 + strlen(p) - 1; while(s1 < s2) { tem = *s1; *s1 = *s2; *s2 = tem; s1++; s2--; } }
3.17找出一个字符串中一个最长的连续数字,并标注出位置和长度。
思路:指针法,指针从字符串开头开始寻找数字,若找到数字,则暂时记下位置,接着不断指向下一个字符,看连续的数字有多长,直到遇到非数字字符,然后比较长度是否比上一个连续数字长,若是则记录位置跟长度,接着寻找下一个数字,直到字符串结尾。最后返回最长的连续数字的位置和长度。
char *find(char *a, int *size) { char *in = a, *temp,*pos; int count = 0, max = 0; while(*in != '\0') { if(*in >= '0' && *in <= '9') // 寻找数字 { temp = in; while(*in >= '0' && *in <= '9') // 判断长度 { count += 1; in++; } if(count > max) // 记录最长连续数字的位置跟长度 { pos = temp; max = count; } count = 0; } in++; } *size = max; return pos; }
思路:
(1)质数是指大于1的自然数中,除了1和它本身不再有其他因数的自然数。一个大于1的自然数不是质数就是合数,因此可以将问题转换为判断合数。
(2)合数一定可以由两个自然数相乘得到,一个小于或等于它的平方根(大于1),另一个大于或等于它的平方根。因此可以判断“2 ~ 输入参数的平方根”中是否有能被输入参数整除的数,若有则该数是合数,若没有则该数是质数。
代码:
int IsPrime (unsigned int p) { unsigned int i; if(p <= 1) { printf("请输入大于1的自然数。\n"); return -1; } for(i = 2; i <= sqrt(p); i++) { if(p % i == 0) { printf("该数不是质数。\n"); // 是合数 return 0; } } printf("该数是质数。\n"); return 0; }
3.19大小端转化:对一个输入的整型数进行大小端存储模式转化
思路:大小端转化就是将一个整型数的低字节放到高字节,高字节放到低字节,跟前面的位翻转类似,只不过这里的单位是字节,因此需要将位翻转中的&0x01改为&0xFF,<< bit改为size * 8,>>= 1改为 >> 8。
代码:
int endian_convert(int input) { int result = 0; int size = sizeof(input); while(size--) { result |= ((input & 0xFF) << (size * 8)); input >>= 8; } return result; }