内存模型:堆与栈
在学习 Java 和 C# 语言过程中, 我们都知道这两种语言都属于托管开发模式, 即:
在运行时由一个管理器(或运行时环境)负责管理应用程序的生命周期、内存分配、资源管理等任务,从而提供更高级别的抽象和便利的开发体验。
在这种的托管模式中, 内存可分为两类: Stack 和 Heap
- Stack : 称为栈, 栈是程序运行时自动分配和释放的一块内存区域,用于存储局部变量、方法参数、返回地址和临时数据等。栈内存的管理是由程序运行时系统自动完成的,因此无需手动申请或释放。每个线程都有一个独立的栈,栈的大小是固定的。当栈空间不足时,会抛出 StackOverflowException 异常。
- Heap : 称为堆, 堆是程序运行时动态分配和释放的一块内存区域,用于存储对象和数组等。堆内存的管理是由程序员手动申请和释放的,需要使用 new 运算符来创建对象。堆内存的大小是不固定的,当堆空间不足时,程序会自动进行垃圾回收(GC)。
在Java 和 C# 语言中,我们将数据类型分为: 基本类型 和引用类型。基本类型一般包括值类型,在内存中存储在栈中。
引用类型在内存中分为 引用 和 实例化的对象,引用存放在栈中,实例化的对象在堆中。
存储示意图如下:
什么是引用?
在 Java 和 C# 中,引用(Reference)存储的是对象的内存地址。当我们创建一个对象时,会在堆上为该对象分配一块连续的内存空间,并返回这块内存空间的地址。当我们创建一个引用时,实际上是在栈上创建了一个变量,这个变量存储了对象在堆上的地址。
例如,在上面的图中代码创建了一个对象和一个引用:
MyClass cls1 = new MyClass();
在这个例子中,new MyClass()
会在堆上分配一块内存空间来存储一个 MyClass 对象,并返回这个对象的内存地址。然后,会将这个内存地址赋值给 cls1
变量,即 cls1
存储了对象在堆上的地址。
当我们使用引用访问对象的属性或方法时,实际上是通过引用存储的对象地址来访问对象的内存空间。例如,下面的代码使用引用访问对象的属性:
cls1.toString();
在这个例子中,cls1.toString()
实际上是通过引用 cls1
存储的对象地址来访问对象的 toString()
方法。
需要注意的是,Java 和 C# 中的引用是一种动态的概念,它可以随时指向不同的对象。例如,下面的代码将一个新的对象赋值给 obj
变量:
cls1 = new MyClass();
在这个例子中,cls1
变量将引用一个新的对象,而之前的 MyClass 对象则成为了孤岛对象,可能会被垃圾回收器回收。
总之,在 Java 和 C# 中,引用存储的是对象的内存地址,它可以随时指向不同的对象,并通过这个地址来访问对象的内存空间。
在C++ 中, 我们可以如下定义一个变量:
int a = 0;
此时 a
是一个变量, 而引用(Reference)是一种别名,它是一个已存在对象的别名,可以用来访问该对象。使用下面的方式定义一个引用:
int& b = a;
在 C++ 中,引用是一个别名,它允许使用一个变量的别名来访问该变量。引用使用 &
符号来声明,并且必须在定义时初始化。
引用有以下几个特点:
- 引用是一个别名,引用变量的值与原变量的值是一样的。
- 引用必须在定义时初始化,一旦初始化后,就不能被重新赋值为另一个变量,只能作为原变量的一个别名来使用。
- 引用在内存中不占用独立的存储空间,只是原变量的另一个名字。
如何理解上述的第2条呢, 看代码:
int main() {
int a = 1;
int& ref = a;
cout << "变量a的值为:" << a << endl;
cout << "引用ref的值为:" << ref << endl;
int b = 10;
ref = b;
b = 3;
cout << "变量a的值为:" << a << endl;
cout << "引用ref的值为:" << ref << endl;
cout << "变量b的值为:" << b << endl;
return 0;
}
上述代码中:
- 声明变量 a 并赋值为 1
- 创建变量 a 的引用 ref
- 输出此时的 a 和 ref 的值
- 声明变量 b 并赋值为 10
ref = b
意为把 b 的值赋值为 ref,即赋值给 a- 赋值 b 的值为3
- 输出 a ref b 的值
结果如下:
变量a的值为:1
引用ref的值为:1
变量a的值为:10
引用ref的值为:10
变量b的值为:3
特别的, 如果引用指向的对象被销毁或改变了地址,那么访问该引用就会出现未定义的行为,可能会导致程序崩溃或其他问题。因此,在使用引用时需要格外注意引用的生命周期和作用域,以避免出现悬垂引用等问题。
什么是指针?
在 C++ 中,指针(Pointer)是一种特殊的变量,它存储了一个内存地址,指向另一个变量或对象。指针可以用来访问和修改指向的变量或对象,它是 C++ 中最重要的概念之一。
指针在 C++ 中的定义方式为:类型* 变量名
,其中 类型
是指针指向的变量或对象的类型,变量名
是指针变量的名称。例如,下面的代码定义了一个指向整数类型的指针:
int* ptr;
在这个例子中,ptr
是一个指向整数类型的指针变量,它存储了一个整数类型变量的地址。即 ptr 只能存储整数类型变量的地址, 下图中 ptr 指向变量 a 的地址可以, 指向 str 的地址会提示编译错误.
通过指针访问对象或者变量,称为 解引用,
未初始化的指针称为 野指针,
初始化为 nullptr 的指针称为 空指针,
如:
int* ptr1; // 野指针
int* ptr2 = nullptr; // 空指针
int a = 10;
int* ptr = &a;
cout << "a :" << *p << endl; // 解引用
指针在 C++ 中有许多重要的用途,例如:
- 动态内存分配:通过
new
运算符可以动态地分配内存空间,并返回指向该内存空间的指针。通过delete
运算符可以释放已经分配的内存空间。 - 函数参数传递:可以通过指针将数据从一个函数传递到另一个函数,或者通过指针返回函数的结果值。
- 数组访问:可以通过指针访问数组中的元素。
- 对象访问:可以通过指针访问对象的属性和方法。
需要注意的是,在使用指针时需要格外小心,因为指针可以指向任何地址,包括未初始化的地址、野指针和已经释放的内存地址等,这些都可能导致程序出错或崩溃。因此,在使用指针时需要格外注意指针的生命周期和作用域,以避免出现悬垂指针、空指针等问题。
可以给指针定义引用么?
答案是: 可以,语法如下:
int* ptr = new int(10); // 创建一个指向整型变量的指针,其值为 10
int*& ref = ptr; // 定义一个指针的引用,它引用指针 ptr
在这个例子中,我们首先创建了一个指向整型变量的指针 ptr
,并将其初始化为一个新的整型变量,它的值为 10
。然后,我们定义了一个指针的引用 ref
,它引用了指针 ptr
。此时,ref
和 ptr
指向同一个内存地址,它们实际上是同一个变量的不同名称。因此,对 ref
的修改会影响到指针 ptr
的值,反之亦然。
指针的引用在 C++ 中通常用于函数参数传递或返回值。通过使用指针的引用,我们可以避免指针拷贝和内存分配的开销,提高程序的性能。同时,使用指针的引用还可以让代码更加简洁和易读。
C++中指针和引用有什么区别?
在 C++ 中,指针和引用都是用来访问内存中的数据的。它们之间的主要区别是:
- 指针可以被重新赋值为指向其他的内存地址,而引用一旦初始化后就不能被重新赋值。也就是说,指针是可以改变其指向的对象,而引用始终指向其初始化时所绑定的对象。
- 指针可以为NULL或nullptr,表示指向一个空地址,而引用必须始终引用一个有效的对象。因此,使用引用时需要注意避免引用空指针或未初始化的变量。
- 对指针进行解引用操作时,需要先判断指针是否为NULL或nullptr,否则可能会导致运行时错误。而引用不需要解引用操作,因为它始终引用一个有效的对象。
- 指针可以进行算术运算,如加、减等操作,以及使用下标访问数组中的元素。而引用不支持这些操作,因为它只是绑定到一个对象上的别名,没有实际的地址和大小。
- 指针可以作为函数参数传递,以及动态分配内存等高级应用。而引用通常用于函数参数传递,因为它可以避免对象被复制,提高程序的效率。
什么是句柄?
句柄(Handle)是一种用于标识某个资源或对象的唯一标识符。在 Windows 操作系统中,句柄通常用于标识系统分配的资源,例如窗口、设备上下文、文件、进程等。
句柄本质上是一个整数值或指针类型,它与资源实体之间的映射关系由操作系统维护。在使用句柄时,可以通过句柄来操作相应的资源或对象,例如读写文件、操作窗口、控制进程等。
Windows 操作系统中有很多种句柄类型,包括窗口句柄、设备上下文句柄、文件句柄、进程句柄等。每种句柄类型都有其特定的用途和操作方法。
在 Windows API 中,可以使用一些函数来获取和操作句柄,例如 FindWindow()
函数用于查找窗口句柄,CreateFile()
函数用于创建文件句柄,OpenProcess()
函数用于打开进程句柄等。这些函数通常返回一个句柄,用于标识相应的资源或对象。
句柄和指针有什么区别?
句柄(Handle)是一个 Windows 操作系统中的概念,而不是 C++ 语言本身的概念。
句柄和指针都可以用来表示一个对象或变量的内存地址,但它们的用途和含义有所不同。
如前面所说:
- 句柄是一种用于标识某个资源或对象的唯一标识符, 只能通过系统函数来访问和操作。
- 指针是一个用于存储变量或对象内存地址的数据类型,它直接指向一个对象或变量的内存地址。
后记
如果你喜欢作者的内容, 请关注我的微信公众号:
或者加入微信群, 一起交流学习, 如二维码过期,请在公众号回复"微信群", 获取最新二维码或者添加我的微信: dev-wiki.