1、以下为WindowsNT 32位C++程序,请计算下面sizeof的值
char str[] = "hello";
char* p = str;
int n = 10;
// 请计算
sizeof(str) = ?
sizeof(p) = ?
sizeof(n) = ?
void Func(char str[100])
{
// 请计算
sizeof(str) = ?
}
void* p = malloc(100);
// 请计算
sizeof(p) = ?
【参考答案】:
sizeof(str) = 6
sizeof()计算的是数组的所占内存的大小包括末尾的 ‘\0’
sizeof ( p)= 4
p为指针变量,32位系统下大小为 4 bytes
sizeof(n) = 4
void Func(char str[100])
{
sizeof(str) = 4
}
函数的参数为字符数组名,即数组首元素的地址,大小为指针的大小
void* p = malloc(100);
sizeof( p) = 4
p指向malloc分配的大小为100 byte的内存的起始地址,sizeof§为指针的大小,而不是它指向内存的大小
2、分析运行下面的Test函数会有什么样的结果
void GetMemory1(char* p)
{
p = (char*)malloc(100);
}
void Test1(void)
{
char* str = NULL;
GetMemory1(str);
strcpy(str, "hello world");
printf(str);
}
char *GetMemory2(void)
{
char p[] = "hello world";
return p;
}
void Test2(void)
{
char *str = NULL;
str = GetMemory2();
printf(str);
}
void GetMemory3(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test3(void)
{
char* str = NULL;
GetMemory3(&str, 100);
strcpy(str, "hello");
printf(str);
}
void Test4(void)
{
char *str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
cout << str << endl;
}
}
【参考答案】:
Test1, 程序崩溃。因为GetMemory1并不能传递动态内存,Test1函数中的 str一直都是NULL。strcpy(str, "hello world")
将使程序奔溃
Test2,可能是乱码。因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,使其原现的内容已经被清除,新内容不可知。
Test3,能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
Test4, 篡改动态内存区的内容,后果难以预料。非常危险。因为 free(str);
之后,str成为野指针,if(str != NULL)
语句不起作用。
3、实现内存拷贝函数 char *strcpy(char* strDest, const char* strSrc);
【参考答案】:
(1)、函数实现
char * strcpy(char *dst,const char *src) //[1]
{
assert(dst != NULL && src != NULL); //[2]
char *ret = dst; //[3]
while ((*dst++=*src++)!='\0'); //[4]
return ret;
}
[1]const修饰
源字符串参数用const修饰,防止修改源字符串。
[2]空指针检查
(A)不检查指针的有效性,说明答题者不注重代码的健壮性。
(B)检查指针的有效性时使用 assert(!dst && !src);
char *
转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。
©检查指针的有效性时使用 assert(dst != 0 && src != 0);
直接使用常量(如本例中的0)会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。
[3]返回目标地址
(A)忘记保存原始的strdst值。
[4]’\0’
(A)循环写成while (*dst++=*src++);
明显是错误的。
(B)循环写成while (*src!='\0') *dst++=*src++;
循环体结束后,dst字符串的末尾没有正确地加上’\0’。
(2).为什么要返回char *?
返回dst的原始值使函数能够支持链式表达式。
链式表达式的形式如:
int l=strlen(strcpy(strA,strB));
又如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。
其一,源字符串肯定是已知的,返回它没有意义。
其二,不能支持形如第二例的表达式。
其三,把const char *
作为char *
返回,类型不符,编译报错。
3.假如考虑dst和src内存重叠的情况,strcpy
该怎么实现
char s[10]="hello";
strcpy(s, s+1);
//应返回ello,
//strcpy(s+1, s);
//应返回hhello,但实际会报错,因为dst与src重叠了,把’\0’覆盖了
所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有一种情况:src<=dst<=src+strlen(src)
C函数memcpy自带内存重叠检测功能,下面给出memcpy的实现my_memcpy。
char * strcpy(char *dst,const char *src)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
my_memcpy(dst, src, strlen(src)+1);
return ret;
}
my_memcpy的实现如下
char *my_memcpy(char *dst, const char* src, int cnt)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
if (dst >= src && dst <= src+cnt-1) //内存重叠,从高地址开始复制
{
dst = dst+cnt-1;
src = src+cnt-1;
while (cnt--)
*dst-- = *src--;
}
else //正常情况,从低地址开始复制
{
while (cnt--)
*dst++ = *src++;
}
return ret;
}
4、按照下面描述的要求写程序
已知String的原型为
class String
{
public:
String(const char *str = NULL); //构造函数
String(const String &other); //拷贝构造函数
~ String(void); //析构函数
String & operate =(const String &other);//赋值函数
private:
char *m_data;// 用于保存字符串
};
请编写上述四个函数。 此题考察对构造函数赋值运算符实现的理解。实际考察类内含有指针的构造函数赋值运算符函数写法。
参考答案:
//构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; //对空字符串自动申请存放结束标志'\0'
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
//析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷贝构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operate =(const String &other)
{
if(this == &other) //检查自赋值
return *this;
delete []m_data; //释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length+1]; //对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this; //返回本对象的引用
}
5、说一说进程的地址空间分布
【参考答案】:
对于一个进程,其空间分布如下图所示
如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。
命令行参数和环境变量: 命令行参数是指从命令行执行程序的时候,给程序的参数。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
文件映射区,位于堆和栈之间。
堆区:动态申请内存用。堆从低地址向高地址增长。
BSS 段:存放程序中未初始化的 全局变量和静态变量 的一块内存区域。
数据段:存放程序中已初始化的 全局变量和静态变量 的一块内存区域。
代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
6、说一说C与C++的内存分配方式?
【参考答案】:
1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量,static变量。
2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3)从堆上分配(动态内存分配)程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存期自己决定,使用非常灵活。
7、new、delete、malloc、free关系
【参考答案】: delete会调用对象的析构函数,和new对应。free只会释放内存,new调用构造函数。malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
8、为什么new/delete 、new []/delete[] 要配对使用?
【参考答案】:
如果是带有自定义析构函数的类类型,用 new [] 来创建类对象数组,而用 delete来释放会发生什么?用上面的例子来说明:
class A {};
A* pAa = new A[3];
delete pAa;
那么 delete pAa;
做了两件事:
- 调用一次
pAa
指向的对象的析构函数; - 调用
operator delete(pAa);
释放内存。
显然,这里只对数组的第一个类对象调用了析构函数,后面的两个对象均没调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。
上面的问题你如果说没关系的话,那么第二点就是致命的了!直接释放pAa指向的内存空间,这个总是会造成严重的 段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地方减去 4 个字节的地方。你应该传入参数设为那个地址!