文章目录
C++ 内存模式
字符串操作函数
1. strcpy(str1, str2)
把从str2地址开始且含有"\0"结束符的字符串 复制到以str1开始的地址空间,返回值类型为char*
str1为NULL时,将使程序崩溃
char* stycpy(char* dest, char* src) {
assert (dest != NULL && src != NULL);
char* ret = *dest
while ((*dest ++ = &src ++) != '\0');
return ret;
}
2. strlen()
计算给定字符串长度
int strlen(const char* str) {
assert (str != NULL);
int len = 0;
while((*str ++) != '\0')
len++;
return len;
}
3. strcat(char* dest, char* src)
把src所指字符串添加到dest的结尾
char* strcat(char* dest, const char* src) {
assert (dest && src);
char *ret = dest;
while(*dest) // 找到dest 的'\0'
dest ++;
while(*dest ++ = *src ++);
return ret;
}
4. strcmp(const char* str1, const char* str2)
int strcmp(const char* str1, const char* str2) {
assert(str1 && str2);
while(*str1 && *str2 && (*str == *str2)) {
str1++;
str2++;
}
return *str1 - *str2;
}
两个字符串自左向右逐个字符相比(按 ASCII 值大小相比较),直到出现不同的字符或遇 \0 为止。如
"A"<"B"
"A"<"AB"
"Apple"<"Banana"
"A"<"a"
"compare"<"computer"
- 若str1 == str2,则返回零
- 若str1 < str2,则返回负数
- 若str1 > str2,则返回正数
内存泄漏 (memory leak)
1. 定义
内存泄漏是指 由于疏忽或者错误操作造成的 程序未能释放掉不再使用的内存的情况。内存泄漏并非是指在物理上的消失,而是应用程序分配某段内存后,由于设计失误,失去了对该段内存的控制,因而造成了内存浪费的情况。
2. 内存泄漏的分类
(1) 堆内存泄漏 (Heap Leak)
堆内存泄漏是指程序运行中,根据需要分配,通过malloc, realloc, new 等从堆中分配的内存,没有经过对应的free, delete 等操作删掉。 堆内存泄漏会产生Heap Leak。
(2)系统资源泄漏 (Resource Leak)
主要是指, 使用程序分配的资源,如Bitmap, handle, SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
(3) 没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是虚函数,那么子类对象的析构函数不会被调用,子类的资源没有正确释放,因此造成内存泄漏
3. 什么操作会造成内存泄漏
指针指向改变,未释放动态分配的内存
4. 如何防止内存泄漏
- 将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;
- 使用只能指针
5. 你对智能指针的了解有哪些?
智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的,C11标准中,放在<memory>头文件中。包括共享指针(shared_ptr), 独占指针(unique_ptr),弱指针(weak_ptr)
6. 构造函数,析构函数要设为虚函数吗,为什么?
(1)析构函数
析构函数需要设为虚函数,当派生类对象中有内存需要回收时,如果析构函数不是虚函数,不会动态绑定,只会调用基类的虚函数,导致派生类的资源无法释放,造成内存泄漏
(2)构造函数
构造函数不需要,没有意义。虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。但是如果要创建一个对象,需要知道对象的完整信息,尤其是想要创建的确切类型,因此构造函数不能定义为虚函数
测试题目
1. 以下为Windows NT 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位系统下大小为4bytes
sizeof(n) = 4; // n是整形变量,占用内存空间4个字节
void Func(char str[100]) {
sieof(str) = 4; // 函数的参数为数组名,即数组首元素的地址,大小为指针大小
}
void* p = malloc(100);
sizeof(p) = 4; // p指向malloc分配的大小为100byte的内存的起始地址,sizeof(p)为指针大小,而不是其所指向的内存大小
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(void):
// 程序崩溃,因为GetMemory1并不能传递动态内存,Test1函数中的str一直都是NULL。 strcpy(str, "Hello World")将使程序崩溃
Test2(void):
// 可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针。该指针的地址不是NULL,使其原有的内容已经被清除, 新内容不可知
Test3(void):
// 能够输出 Hello,但是存在内存泄漏,GetMemory3申请的内存没有释放
Test4(void):
// 篡改动态内存区的内容, 后果难以预料。非常危险。因为free(str)之后,str成为野指针,if (str != NULL)语句不起作用。
3. 实现内存拷贝函数
char* strcpy(char* strDest, const char* strSrc);
参考答案:
char* strcpy(char* dest, const char* src) { // [1]
assert (dest != NULL && src != NULL); // [2]
char *ret = dest; // [3]
while ((*dest ++ = *src++) != '\0'); // [4]
return ret;
}
[1] const 修饰符
源字符串用const 修饰符,防止修改源字符串
[2] 空指针检查
(1)不检查空指针的有效性,说明答题者不注重代码的健壮性
(2)检查指针的有效性时,使用assert (!dest && !src);
char* 转换为bool类型即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错的概率增大和维护成本的升高。
(3)检查指针有效时使用assert (dest != 0 && src != 0);
直接使用常量会减少代码的可维护性,而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。
[3] 返回目标地址
不能忘记保存原有地址值
[4] ‘\0’
(1)循环写成while (*dest ++ = *src++)
明显是错误的
(2)循环写成while (*src != '\0') *dest ++= *src ++;
循环体结束后,dest字符串末尾没有正确的加上 ‘\0’
(3)为什么要返回 char *
返回dest的原始值,使得函数能够支持链式表达式
链式表达式的形式如:
int l = strlen(strcpy(strA, strB));
又如:
char * strA = strcpy(new char[10], strB);
返回strSrc的原值是错误的
理由:
- 源字符串肯定是已知的,返回没有意义
- 不能支持形如第二例的表达式
- 把
const char*
作为char *
返回,类型不符,编译报错
4. 假如考虑dest和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
自带内存重叠检测功能,如下所示:
char* strcpy(char* dest, const char* src) {
assert (dest != NULL && src != NULL);
char* ret = dest;
my_memcpy(dest, src, strlen(src) + 1);
return ret;
}
//my_memcpy 实现如下:
char* my_memcpy(char* dest, const char* src, int cnt) {
assert (dest != NULL && src != NULL);
char *ret = dest;
if (dest >= src && dst <= src + cnt - 1) {
dest = dest + cnt - 1;
src = src + cnt - 1;
while(cnt --)
*dest -- = *src --;
}
else {
while (cnt --)
*dest++ = *src ++;
}
return ret
}
5. 按照下面描述的要求写程序:
已知String的原型为:
class String {
public:
String(const char *str = NULL);
String(const String &other);
~ String(void);
String & operate =(const String &other);
private:
char *m_data;
};
请编写上述4个函数。
参考答案:
// 构造函数
String::String(const char* src) {
if (src == NULL) {
m_data = new char[1];
m_data[0] = '/0';
}
else: {
int length = strlen(src);
m_data = new char[length + 1];
strcpy(m_data, src);
}
}
// 析构函数
String::~String(void) {
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(othrt.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
return *this;
}
6. 说一说进程地址空间分布
参考答案:
对于一个进程而言,其空间分布如下图所示:
如上图,从高地址到低地址,一个进程的地址空间分布为:命令行参数和环境变量、栈、文件映射区、堆、BSS段,Data段,代码段 组成。
(1)命令行参数和环境变量
命令行参数是指从命令行执行长度时,给程序的参数。
(2)栈区
存储局部变量、函数参数值。 栈从高地址空间向低地址空间增长。 是一块连续的空间、
(3)文件映射区
位于堆和栈之间
(4)堆区
用于动态申请内存。 对从低地址空间向高地址空间增长。
(5)BSS段
存放程序中 未初始化的全局变量和静态变量 的一块内存
(6)Data段
存放程序中 已经初始化的全局变量和静态变量 的一块内存
(7) 代码段
存放程序执行代码的一块内存区域。 只读, 代码段的头部还会包含一些只读的常量变量。
7. 说一说 C 与 C++ 的内存分配方式
(1)从静态存储区分配
内存在 程序编译时就已经分配好, 这块内存在程序的整个运行期间都存在,如全局变量,static变量。
(2)在栈上创建
在执行程序时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存空间有限。
(3)在堆上创建 (动态内存分配)
程序在运行的时候,用malloc 或者 new 申请任意多少的内存,程序员负责在何时用free 或 delete 释放内存。动态内存的 生存期程序员来决定,使用十分灵活。
8. new, delete, malloc, free的关系
参考答案:
如果是带有自动以析构函数的类型,用new [] 来创建类对象数组,而delete来释放会发生什么?举例如下:
class A {};
A* pAa = new A[3];
delete pAa;
那么delete pAd;
做了两件事:
- 调用一次pAa指向的对象的析构函数
- 调用
operator delete(pAa);
释放内存
显然,这里只对数组的第一个类对象调用了析构函数,后面两个对象均没有调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。
上面的问题,如果说还ok的话,那么第二点就致命了。直接释放掉pAa指向的内存空间,这个总是会造成严重的段错误
,程序必然会崩溃! 因为分配空间的起始点是pAa指向的区域,减去4个字节的地方,你应该传入参数设置为那个地址!