【C++中的内存管理】

是什么在召唤着我,提醒我,我懦弱过犹豫过..........................................................................

目录

前言

一、【C++中管理动态内存的方式】

1.1【new和delete的引入】

1.2【new和delete的使用】

1.【针对内置类型】

2.【针对自定义类型】

1.3【operator new与operator delete】

1、new/delete和operator new/operator delete之间的关系

2、operator new和operator delete的原理

1.3【new和delete的实现原理】

1.【内置类型】

2.【 自定义类型】

new的原理

delete的原理

new T[N]的原理

delete[ ]的原理

1.4【定位new表达式(placement-new) 】

1、使用格式

2、使用场景

1.5【malloc/free和new/delete的区别与联系】

1、共同点

2、区别

1.6【内存泄漏】

1、什么是内存泄漏

2、内存泄漏的危害

3、内存泄漏分类

1.堆内存泄漏(Heap leak)

2.系统资源泄漏

4、如何检测内存泄漏

5、如何避免内存泄漏

6、如何一次在堆上申请4G的内存?

二、【类在内存中的存储分布】

2.1、【问题引入】

2.2、【回顾】

2.3、【探究】

1、【单个类的内存分布】

2、【多类的内存分布(继承,组合)】

注意:

总结


前言

本篇是关于C++中的内存管理,可以帮助我们学习管理程序所消耗的内存资源,请耐心观看。


一、【C++中管理动态内存的方式】

1.1【new和delete的引入】

我们之前已经学过C语言中的动态内存管理,了解什么是动态内存管理,由于C++支持C语言所以C语言管理动态内存的方式在C++中依然适用,像malloc,calloc,realloc这些函数在C++中仍然适用。就比如在C语言中我们可能会像下面那样申请空间。

请注意下面的代码是在默认对内存的申请会成功的情况下:

void Test_C_storage()
{
    //静态开辟空间
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//申请10个整形大小空间的数组,40字节
	char crr[10] = {'a','b','c','d'};//10个字符大小的数组,10字节
    //动态开辟空间
	int* ptr = (int*)malloc(sizeof(arr) / sizeof(arr[0]) * 20);//动态申请20个整形大小的空间
    //动态开辟一个二维数组
	int** parr = (int**)malloc(sizeof(int*) * 3);
	for (int i = 0; i < 3; i++)
	{
		parr[i] = (int*)malloc(sizeof(int) * 5);
	}
    //扩容
    int* newptr = (int*)realloc(ptr,sizeof(arr) / sizeof(arr[0]) * 25);
	ptr = newptr;
    //开辟空间并初始化
    char* str = (char*)calloc(10, sizeof(char));//开辟10个字符大小的空间
	for (int i = 0; i < 10; i++)
	{
		str[i] = 'a' ;//并对其初始化为’a‘
	}
	cout << endl;

    
}

但是这里有一个明显的问题就是如果针对于内置类型,可以直接申请空间然后对其进行初始化,但是C++是OOP语言,涉及的不在单纯是内置类型了,还有自定义类型。而对于自定义类型却只能做到申请空间而无法直接进行初始化,所以C++的祖师爷就创造了new来申请和管理C++中的动态内存,和delete对申请的内存空间进行释放,就类似于C语言中的malloc和free,但是在介绍new和delete之前我们要先知道,内存分为哪些区域,各区域又是做什么的。

下面让我们看一份代码:

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof (int)* 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int)* 4);
	free(ptr1);
	free(ptr3);
}

对于上面代码中的各个变量,可以通过下图看其在内存当中的的存储位置

【说明】
1. 栈又叫堆栈一般是用来存储——非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是向上增长的。
4. 数据段——存储全局数据和静态数据。
5. 代码段又叫做常量区——可执行的代码(函数)/只读常量。

下面说一下为什么栈是向下增长而堆是向上增长的

在一般情况下,在栈区开辟空间,先开辟的空间地址较高,而在堆区开辟空间,先开辟的空间地址较低。所以一直在栈开辟空间,开辟的空间按地址就持续降低,同理在堆上一直开辟空间,空间地址就一直升高。

下面我们看一个例子:

int main()
{
	//栈区开辟空间,先开辟的空间地址高
	int a = 10;
	int b = 20;
	cout << &a << endl;
	cout << &b << endl;

	//堆区开辟空间,先开辟的空间地址低
	int* c = (int*)malloc(sizeof(int) * 10);
	int* d = (int*)malloc(sizeof(int) * 10);
	cout << c << endl;
	cout << d << endl;
	return 0;
}

1、因为在栈区开辟空间,先开辟的空间地址较高,所以打印出来a的地址大于b的地址;

2、而在堆区开辟空间,先开辟的空间地址较低,所以c指向的空间地址小于d指向的空间地址。

注意:

在堆区开辟空间,后开辟的空间地址不一定比先开辟的空间地址高。因为在堆区,后开辟的空间也有可能位于前面某一被释放的空间位置。

1.2【new和delete的使用】

首先我们要知道new是向堆上申请空间,并且返回的是指针类型,具体使用例子如下:

1.【针对内置类型】

来看看new和delete在内置类型当中的使用吧。

#include <stdio.h>
#include <iostream>
using namespace std;


void Test()
{
 // 动态申请一个int类型的空间
   int* ptr4 = new int;
 // 动态申请一个int类型的空间并初始化为10
   int* ptr5 = new int(10);
 // 动态申请10个int类型的空间
    int* ptr6 = new int[3];
    delete ptr4;
    delete ptr5;
    delete[] ptr6;
}
int main()
{
	int* ptr1 = new int;//单纯申请空间,
	int* ptr2 = new int(10);//不仅申请空间,还给这块空间初始化为10.
	int* ptr3 = new int[3];//申请连续的空间大小为3
	int* ptr4 = new int[3]{ 0,1,2 };//申请连续的空间并为其初始化.
	delete ptr1;//释放申请的空间,注意这里最好配套使用,也就是说new-->delete,new[]--->delete[];
	delete ptr2;
	delete[] ptr3;
	delete[] ptr4;//
	return 0;
}

注意

申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用new[ ]和delete[ ]。

2.【针对自定义类型】

针对于自定义类型new和malloc有什么区别呢?

看下面的代码:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A的构造函数" << endl;
	}
	~A()
	{
		cout << "A的析构函数" << endl;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	a1->Print();
	A* a2 = new A(2);
	a2->Print();
	free(a1);
	delete(a2);

	return 0;
}

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};


int main()
{
	// new/delete 和 malloc/free最大区别是 
    //new/delete对于自定义类型除了开空间
	//还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;
	// 内置类型是几乎是一样的
	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	free(p3);
	delete p4;
    //连续为自定义类型申请空间
	A* p5 = (A*)malloc(sizeof(A) * 10);
	A* p6 = new A[10];
	free(p5);
	delete[] p6;
	return 0;
}

注意:

在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

总结:

1、C++中如果是申请内置类型的对象或是数组,用new/delete和malloc/free没有什么区别。
2、如果是自定义类型,区别很大,new和delete分别是开空间+构造函数、析构函数+释放空间,而malloc和free仅仅是开空间和释放空间。
3、建议在C++中无论是内置类型还是自定义类型的申请和释放,尽量都使用new和delete。


                  

1.3【operator new与operator delete】

1、new/delete和operator new/operator delete之间的关系

new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete是
系统提供的全局函数
new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。

	int* p1 = (int*)operator new(sizeof(int)* 10); //申请
	
	operator delete(p1); //销毁

就类似于:

	int* p2 = (int*)malloc(sizeof(int)* 10); //申请
	
	free(p2); //销毁

2、operator new和operator delete的原理

这里需要注意的是,operator new并不是new的重载,反而可以看成new的一部分,也就是说new是通过operator new进行申请空间的:

operator new的底层是通过调用malloc函数来申请空间的,当malloc申请空间成功时直接返回;若申请空间失败,则尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。而operator delete的底层是通过调用free函数来释放空间的。


虽然说operator new和operator delete是系统提供的全局函数,但是我们也可以针对某个类,重载其专属的operator new和operator delete函数,进而提高效率。

可以看下面的例子:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A的构造函数" << endl;
	}
	~A()
	{
		cout << "A的析构函数" << endl;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	a1->Print();
	A* a2 = new A(2);
	a2->Print();
	A* a3 = (A*)operator new(sizeof(A));//跟malloc的使用方法相同.
	a3->Print();
	free(a1);
	delete(a2);
	operator delete(a3);
	return 0;
}

1.3【new和delete的实现原理】

1.【内置类型】

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

2.【 自定义类型】


new的原理


1. 调用operator new函数申请空间
2. 在申请的空间上执行对应的构造函数,完成对象的构造


delete的原理


1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间


new T[N]的原理


1. 调用operator newT[N]函数,在operator newT[N]中实际调用operator new 函数完成N个T对象空间的申请。
2. 在申请的空间上执行N次构造函数


delete[ ]的原理


1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释
放空间。

下面我们来看一个问题,这里测试平台是32位下VS2019:

我们知道当我们使用new T[N]时,本质上是调用operator newT[N],然后operator newT[N]再调用operator new N次完成对N个T对象的空间的申请,接着会调用N次T对象的构造函数。

那么我们为什么一定进行匹配使用呢?

首先对于new T[N],使用delete[ ],显然没有问题,但是如果使用free和delete呢?

下面来看这样一个例子:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	//~A()
	//{
	//	cout << "~A()" << endl;
	//}
private:
	int _a;
};
int main()
{
	A* p = new A[10];
	delete p;
	//free(p);
	//delete[] p;
	return 0;
}

我们发现在上面给出的例子中并没有写A的析构函数,但是我们发现使用free,delete好像也可以使用这是为什么呢?

原来编译器十分聪明,编译器发现A并没有写析构函数,那么就直接释放了p而对那10个A对象的空间并没有调用析构函数。这样会导致内存泄漏,不建议使用,应该匹配使用。

那如果我们写了析构函数呢?

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = new A[10];
	delete p;
	//free(p);
	//delete[] p;
	return 0;
}

这里A对象显示写了析构函数为什么就不能使用delete和free了呢?
原来当我们显示的写析构函数时,在使用new A[10],进行申请空间时编译器其实在我们需要的空间前面多开了4个字节,用来存10,这样是为了在调用delete[ ]时能够明确调析构函数的次数,而p的位置经过编译器的处理会指向我们需要空间的开始。如下图这样在我们调用delete[ ]时会根据前四个字节存的值调用析构函数。

但是当我们使用delete p时,p指向的却是却是10的位置,要释放的位置不对,因此会报错。

1.4【定位new表达式(placement-new) 】


定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。


1、使用格式

new (place_address) type或者new (place_address) type(initializer-list)
//place_address必须是一个指针,initializer-list是类型的初始化列表

2、使用场景


定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

这里举个例子:
 

class A
{
public:
	void Print()
	{
	    cout << _a << endl;
	}
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{}
private:
	int _a;
};
// 定位new
int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A(2);// 注意:如果A类的构造函数有参数时,此处需要传参
	p1->Print();
	p1->~A();//析构函数也可以显示调用
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->Print();
	p2->~A();
	return 0;
}

注意

在未使用定位new表达式进行显示调用构造函数进行初始化之前,malloc申请的空间还不能算是一个对象,它只不过是与A对象大小相同的一块空间,因为构造函数还没有执行。

1.5【malloc/free和new/delete的区别与联系】

1、共同点

都是从堆上申请空间,并且需要用户手动释放。

2、区别


1. malloc和free是函数,new和delete是操作符
2. malloc申请的空间不会初始化,new可以初始化
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[ ]中指定对象个数即可
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
要捕获异常。
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

1.6【内存泄漏】

1、什么是内存泄漏


内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。


2、内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
 // 1.内存申请了忘记释放
 int* p1 = (int*)malloc(sizeof(int));
 int* p2 = new int;
 // 2.异常安全问题
 int* p3 = new int[10];
 Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
 delete[] p3;
}

3、内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

1.堆内存泄漏(Heap leak)


堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。


2.系统资源泄漏


指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

4、如何检测内存泄漏


在vs下,可以使用windows操作系统提供的“ _CrtDumpMemoryLeaks() ” 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

int main()
{
	int* p = new int[10];
	// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
	_CrtDumpMemoryLeaks();
	return 0;
}

// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
//Detected memory leaks!
//Dumping objects ->
//{79} normal block at 0x00EC5FB8, 40 bytes long.
//Data: <         > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
//Object dump complete.

因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。

5、如何避免内存泄漏


1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。
总结一下:
内存泄漏非常常见,解决方案分为两种:

1、事前预防型:如智能指针等。

2、事后查错型:如泄漏检测工具。

6、如何一次在堆上申请4G的内存?

在堆上申请4G的内存:

#include <iostream>
using namespace std;
int main()
{
	//0xffffffff转换为十进制就是4G
	void* p = malloc(0xfffffffful);
	cout << p << endl;

	return 0;
}

在32位的平台下,内存大小为4G,但是堆的总大小只占了其中的2G左右,所以我们不可能在32位的平台下,一次性在堆上申请4G的内存。这时我们可以将编译器上的win32改为x64,即64位平台,这样我们便可以一次性在堆上申请4G的内存了。

动态内存管理的知识到这里就算结束了,下面是扩展内容,探究类在内存当中是如何存储的。

二、【类在内存中的存储分布】

2.1、【问题引入】

我们知道C++是祖师爷本贾尼是在C语言的基础上进行的创新,C语言是一门面向过程的语言,而C++是一门面向对象的语言,那么这个面向对象体现在哪呢?

主要还是体现在C++引入了类的概念。有了类,C++就可以实现对象与对象进行交互,相比于C语言中的结构体,类同结构体一样都能在自己内部定义各种类型的成员变量(结构体的成员变量可以是结构体这种行为叫做结构体嵌套,同样类的成员变量也可以是类我们一般称之为组合),不同的是类中的成员变量拥有自己的属性并且C++中的类除了能定义成员变量以外还可以定义自己的成员方法,还可以继承,还可以多态,因此类是C++实现面向对象编程(OOP)的不可或缺的一部分。

那么类中的各个成员是存在内存中哪个部分呢?具体是如何分布的呢?下面让我们一块探索。

2.2、【回顾】

我们先来回顾一下C语言中的结构体是如何在内存中进行存储的,我们知道单纯的写一个结构体声明实际上不会占据存储空间比如下面的:

struct Student
{
	int _age;
	char* _name;
};

只有当我们根据这个声明示例化出对象时才会占用内存中的存储空间,比如这里举个例子:
 

struct Student
{
	int _age;
	const char* _name;
	static int _num;
};
int Student::_num = 110;
int j = 0;//全局变量存储在数据段
int main()
{
	struct Student s;//定义s对象,存在与main函数中对应为栈
	s._age = 22;
	s._name = "Alex";
	s._num = 120;

	struct Student* ps = (struct Student*)malloc(sizeof(struct Student));//动态申请对应为堆
	if (ps == NULL)
	{
		perror("malloc failed");
		return 0;
	}
	ps->_age = 32;
	ps->_name = "Bob";
	ps->_num = 119;


	const char* cp = "hello,world";//hello,world为常量字符串,cp为字符串首字符的地址,存储在代码段,cp本身存储在栈区
	int i = 0;//局部变量,栈区
	int* pk = (int*)malloc(sizeof(int));
	printf("数据段地址:%p\n", &j);
	printf("代码段地址:%p\n", (void*)cp); // 转换为void*以避免警告,因为cp的类型是const char* 。
	printf("栈区的地址为%p\n", &i);
	printf("堆区的地址为%p\n", pk);
	cout << endl << endl;

	printf("s的地址为%p\n", &s);
	printf("s中_age的地址为%p,值为%d\n", &(s._age), s._age);
	printf("s中_name的地址为%p,值为%s\n", (void*)&(s._name), s._name);
	printf("s中_num的地址为%p,值为%d\n", &(s._num), s._num); // 正确获取静态成员变量的地址
	cout << endl << endl;

	printf("ps为%p\n", ps);
	printf("ps中_age的地址为%p,值为%d\n", &(ps->_age), ps->_age);
	printf("s中_name的地址为%p,值为%s\n", (void*)&(ps->_name), ps->_name);
	printf("ps中_num的地址为%p,值为%d\n", &(ps->_num), ps->_num); // 正确获取静态成员变量的地址
	cout << endl << endl;

	printf("Student中_num的地址为%p,值为%d\n", &Student::_num, Student::_num); // 正确获取静态成员变量的地址


	return 0;
}

现在我们知道了内存是如何分布的那么内存内存大小该如何计算呢?

这里参考【C语言中的自定义类型——【C语言中的自定义类型】-CSDN博客】,其中的【结构体内存对齐和大小计算】,这里不再回顾。

接下来让我们看一下C++中类的各个成员在内存重视如何分布的。

2.3、【探究】

不同于C语言中的结构体,C++中的类除了可以定义成员变量之外还可以定义成员函数,下面就让我们看一下他们是如何分布的吧!

1、【单个类的内存分布】

我们知道类的成员主要包括成员变量和成员函数,那么它们分别存在于哪里呢?

下面看这样一个例子:

class Student
{
public:
	Student(int age = 18, string name = "Alex")
		:_age(age),
		_name(name)
	{}
	void Print()
	{
		cout << _age << " " << _name << " " << endl;
	}
public://设置为公有以便在类外访问
	int _age;
	string _name;
	static int _id;
};
int j = 0;
int Student::_id = 224;
int main()
{

	const char* cp = "hello,world";//hello,world为常量字符串,cp为字符串首字符的地址,存储在代码段,cp本身存储在栈区
	int i = 0;//局部变量,栈区
	int* pk = (int*)malloc(sizeof(int));
	printf("数据段地址:%p\n", &j);
	printf("代码段地址:%p\n", (void*)cp); // 转换为void*以避免警告,因为cp的类型是const char* 。
	printf("栈区的地址为%p\n", &i);
	printf("堆区的地址为%p\n", pk);
	cout << endl << endl;
	Student s(20, "Jack");
	printf("s的地址%p\n", &s);
	printf("s中成员变量的地址%p,值为%d\n", &(s._age),s._age);
	printf("Student中静态成员变量的地址%p,值为%d\n", &(s._id),s._id);
	printf("Student中成员函数的地址%p\n", &Student::Print);
	cout << endl << endl;


	return 0;
}

这里搞清楚了内存分布,那么大小该如何计算呢?我们知道成员函数并没有存放在类中,所以类的大小主要是成员变量,那么毫无疑问类的大小计算方式也类似于结构体的大小计算方式,都遵循结构体的内存对齐原则。

静态成员函数:

对于静态成员函数而言,其参数中没有this指针。

为什么静态成员函数没有this指针?——因为作为静态成员函数,它是被整个类共享的,不存在需要识别具体对象的情况。

由此我们可以解释为什么静态成员函数可以直接通过类名访问。因为静态成员函数不需要this指针指向具体对象,换句话说,静态成员函数内部不应该使用实例化对象的成员变量,因为无法通过this指针指向具体变量,势必报错。

2、【多类的内存分布(继承,组合)】

我们清楚了单个类的存储,存在继承关系的类又是什么情况呢?

下面看这样一个例子:
 

class Person 
{
public:
    Person(string name)
        : _name(name)
    {}
    void Cout()
    {
        cout << "Person成员函数" << endl;
    }
public:
    string _name;
};

class Student : public Person 
{
public:
    // 基类的构造函数通过成员初始化列表进行初始化  
    Student(int age = 18, string name = "Alex")
        : Person(name), // 正确的基类初始化方式  
        _age(age)
    {}

    void Print() 
    {
        cout << _age << " " << _name << endl;
    }

public: // 设置为公有以便在类外访问  
    int _age;

    // 静态成员变量需要在类外部定义和初始化  
    static int _id;
};

// 在类外部定义和初始化静态成员变量 _id  
int j = 0;
int Student::_id = 224;
int main()
{

	const char* cp = "hello,world";//hello,world为常量字符串,cp为字符串首字符的地址,存储在代码段,cp本身存储在栈区
	int i = 0;//局部变量,栈区
	int* pk = (int*)malloc(sizeof(int));
	printf("数据段地址:%p\n", &j);
	printf("代码段地址:%p\n", (void*)cp); // 转换为void*以避免警告,因为cp的类型是const char* 。
	printf("栈区的地址为%p\n", &i);
	printf("堆区的地址为%p\n", pk);
	cout << endl << endl;
	Student s(20, "Jack");
	printf("s的地址%p\n", &s);
	printf("s中成员变量的地址%p,值为%d\n", &(s._age),s._age);
	printf("s中继承Person的成员变量的地址%p,值为%s\n", &(s._name),s._name.c_str());
    printf("Student中成员函数的地址%p\n", &Student::Print);
    printf("Person中成员函数的地址%p\n", &Person::Cout);
	cout << endl << endl;


	return 0;
}

因此无论是单继承还是多继承,本质上子类当中只存储了父类的非静态成员变量,所以在计算大小时只需要根据结构体的内存对齐规则计算即可,而对于组合类计算其大小也是类似于嵌套结构体一样进行计算,关于嵌套结构体的计算参见 C语言中的自定义类型——【C语言中的自定义类型】-CSDN博客】,其中的【结构体内存对齐和大小计算】。

3、注意

关于菱形虚拟继承和多态当中关于数据冗余和虚函数存储的相关问题请参见博客:

【继承和多态】-CSDN博客 】,这里只是简单了解一下类中的成员函数和成员变量在内存中是如何分布的。


总结

本篇博客到这里就结束了,内容主要是针对于动态内存管理和类成员在内存中的分布。

学疏才浅,还请指正。


..........................................................................我根本没有来过,来过,都怪我亲手杀了骆驼

                                                                                                                     ————《骆驼》

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值