【C++内存管理】C++指针快速入门

本文介绍了C++中的指针概念,包括指针的申请、释放内存,以及指针的大小。详细讨论了程序内存地址划分,堆栈空间初始化,C++11的新式for循环,并展示了如何遍历二维数组。同时,文章涵盖了void指针、指针类型转换和常量指针的相关知识,以及通过示例展示了数组操作图像的过程。
摘要由CSDN通过智能技术生成

1. 第一个指针程序

1.1 指针的概念

指针是 C++ 中的一种变量,其存储的是内存的地址。它指向的内存位置、大小和类型取决于指针的声明。指针可以申请新的内存,也可以指向已有的内存。

1.2 申请和指向内存

你可以使用 new 运算符为指针申请新的内存,或者让它指向一个已经存在的变量。使用 & 运算符可以获取变量的地址,此处的 & 读作 “取地址”。

int *p1 = new int; // 申请新的内存,并将其地址赋给 p1
*p1 = 101; // 将 101 存储到 p1 所指向的内存中

int i = 102;
int* p2 = &i; // 让 p2 指向变量 i 的内存
*p2 = 103; // 通过 p2 修改 i 的值

1.3 释放内存

你可以使用 delete 运算符释放指针所申请的内存。在释放之后,指针变量依然存在,但是其指向的内存已经被回收,不再可用。

delete p1; // 释放 p1 所指向的内存

1.4 指针的大小

无论指针的类型如何,它本身的大小是固定的。这是因为指针变量存储的是内存地址,而不是实际的数据。在 32 位系统中,指针的大小通常是 4 字节,在 64 位系统中,指针的大小通常是 8 字节。

cout << "sizeof(p1): " << sizeof(p1) << endl; // 输出指针 p1 的大小
cout << "sizeof(*p1): " << sizeof(*p1) << endl; // 输出 p1 所指向的数据的大小

2. 程序的内存地址划分

C++ 程序的内存通常被划分为以下几个区域:

  • 保留区:这个区域是为了防止程序访问空指针导致的错误而设立的,通常位于 0 地址。
  • 代码区:也被称为 .text 区,存储程序的机器指令。
  • 全局/静态存储区:包含了 .data(已初始化的全局变量和静态变量)和 .bss(未初始化的全局变量和静态变量)。
  • 堆区:程序在运行时可以从堆区动态分配内存和释放内存。
  • 栈区:用于存储函数的局部变量、函数参数以及返回地址等。
  • 内核区:存储系统级别的指令和数据。

3. 进程的内存与指针代码关系

指针代码直接操作内存,对应进程的内存布局。对全局、局部变量的访问,实质上是对静态存储区和栈区的访问。动态内存分配的new、delete操作,实质上是对堆区的操作。这就构成了进程内存与指针代码的直接关系。

4. 堆栈空间的初始化

4.1 栈空间

栈空间是程序自动分配和释放的,用于存放函数的局部变量、参数等。对于栈空间的数组,我们可以使用 memset 函数将其全部初始化为 0。

int arr1[10]; 
memset(arr1, 0, sizeof(arr1)); // 使用 sizeof 获取数组的大小

你也可以在定义数组时直接初始化数组的元素:

int arr2[5] = { 1, 2, 3, 4, 5 }; // 初始化一个大小为 5 的数组

4.2 堆空间

堆空间是由程序员负责管理的,我们可以使用 newdelete 在堆上分配和释放内存。堆空间的数组同样可以使用 memset 进行初始化,需要注意的是,对于非字符类型的数组,我们需要将 memset 的第三个参数乘以元素的大小。

int *parr1 = new int[1024]; // 在堆上分配一块能存放1024个int的内存
memset(parr1, 0, 1024 * sizeof(int)); // 将内存全部初始化为 0

在完成对堆空间的操作后,记得使用 delete[] 释放内存,并将指针设置为 nullptr,以防止野指针的产生。

delete[] parr1;
parr1 = nullptr;

5. C++11中的for遍历

C++11 引入了新的 for 循环语法,使得遍历数组和容器变得更加简单。以下是使用新的 for 循环语法遍历数组的例子:

int arr[] = { 1, 2, 3, 4, 5 };
for (int x : arr) {
    cout << x << " ";
}

在这个例子中,x 会依次取 arr 中的每一个元素的值,并在循环体中执行操作。这种 for 循环的形式也被称为范围 for 循环,它可以使你的代码更简洁,更易于阅读。

6. 二维数组的内存分配

6.1 栈上的二维数组

在栈上分配的二维数组需要在编译时确定其大小,我们不能在运行时动态改变其大小。在这个程序中,我们在栈上创建了多个二维数组,并用不同的方式进行初始化:

unsigned char arr1[2][ARRSIZE] = { {1,2,3},{4,5,6} };
unsigned char arr2[][ARRSIZE] = { 
    {1,2,3},
    {2,3,4},
    {3,4,5} };

这两个数组都是在栈上分配的,但是arr2的行数是由编译器根据初始化列表推断出来的。

同样地,我们还创建了两个三维数组:

int arr3[2][3][4] =
{
    {{1,2,3,4},{2,3,4,5},{1,2,3,4}},
    {{1,2,3,4},{2,3,4,5},{1,2,3,4}},
};
int arr4[][3][4] =
{
    {{1,2,3,4},{2,3,4,5},{1,2,3,4}},
    {{1,2,3,4},{2,3,4,5},{1,2,3,4}},
    {{1,2,3,4},{2,3,4,5},{1,2,3,4}},
};

6.2 堆上的二维数组

相比于栈,堆提供了更大的灵活性,我们可以在运行时动态分配任意大小的内存。在这个程序中,我们在堆上创建了多个一维数组:

int * parr1 = new int[1024];
auto parr2 = new unsigned char[psize];
auto parr3 = new int[psize];

6.3. 二维数组的遍历

在C++中,我们可以使用嵌套的for循环来遍历二维数组:

for (auto arr : arr2)
{
    for (int i = 0; i < ARRSIZE; i++)
    {
        cout << static_cast<int>(arr[i]) << " " << flush;
    }
    cout << endl;
}

此外,我们还可以使用基于范围的for循环来遍历数组:

for (auto s : str1) //只能访问数组
{
    cout << s << "-"<<flush;
}

6.8 二维数组的清理

对于在栈上分配的数组,它们会在离开作用域时自动被销毁,不需要我们手动清理。而对于在堆上分配的数组,我们需要使用delete[]

操作符手动进行清理,并且建议在清理后将指针设置为nullptr以避免野指针:

delete[] parr1;
parr1 = nullptr;
delete[] parr2;
parr2 = nullptr;

7. Void指针和指针类型转换

7.1 Void指针

void指针,又称为通用指针,可以指向任何类型的数据。以下代码展示了如何使用void指针:

int num = 1;
void* ptr = &num;

在上面的代码中,我们创建了一个void指针,并使其指向一个整数。

7.2 static_cast转换

static_cast是C++中最通用的转换方式,可以用来进行各种有关联性的类型转换。比如,我们可以使用static_cast将void指针转换为int指针:

int* ptr2 = static_cast<int*>(ptr); //void *

然而,static_cast有一些限制,它不能去掉const限定符,也不能转换无关联的类型。

7.3 C语言风格的转换

除了static_cast之外,我们还可以使用C语言风格的转换方式:

int* ptr3 = (int*)ptr;

这种转换方式没有类型检查,更容易出错,所以在C++中不推荐使用。

7.4 const_cast转换

const_cast主要用于去掉变量的const限定:

const int* cptr1 = new int[1024];
int* ptr6 = const_cast<int*>(cptr1); // 去掉 const

在上面的代码中,我们使用const_cast将一个const int指针转换为int指针。

7.5 reinterpret_cast转换

reinterpret_cast用于进行一些高级的、危险的转换,它可以将任何类型的指针转换为任何其他类型的指针:

unsigned char* ucptr = new unsigned char[1024];
auto ptr7 = reinterpret_cast<int*>(ucptr); // 替换指针类型

在上面的代码中,我们使用reinterpret_cast将一个unsigned char指针转换为int指针。

注意,尽管这些转换看起来很强大,但是如果使用不当,它们都有可能导致错误,所以在使用时要特别小心。

8. 常量指针和指针常量

8.1 常量指针

常量指针是指向常量的指针。它可以指向不同的地址,但是不能通过指针来修改其所指向的值。以下代码展示了如何使用常量指针:

const int i1 = 100;
const int* pi1 = &i1; // 指向常量的指针
//(*pi1)++; 指向的值不可修改

在上面的代码中,我们创建了一个指向常量的指针,然后试图通过指针修改其所指向的值,这是不被允许的。

8.2 指针常量

指针常量是指针本身是常量的指针。它可以修改其所指向的值,但是不能指向不同的地址。以下代码展示了如何使用指针常量:

int* const pi3 = new int;
//pi3++; 指针本身是常量 指向不能修改
*pi3 = 200; // 指向的值可以修改
delete pi3;

在上面的代码中,我们创建了一个指针常量,然后试图使其指向另一个地址,这是不被允许的。

8.3 常量指针和指针常量的区别

常量指针和指针常量是两种不同的概念。前者的指针可以改变,指向不同的地址,但是不能通过指针来修改其所指向的值;而后者的指针不可以改变,不能指向不同的地址,但是可以修改其所指向的值。

8.4 const修饰的指针常量

当const同时修饰指针和指针所指向的值时,该指针既不能改变指向,也不能修改其所指向的值:

const int* const pi4 = &i1; // 指向和值都不能修改

在上面的代码中,我们创建了一个指针,既不能改变指向,也不能修改其所指向的值。

9. 数组操作图像示例

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main()
{
	// 打印消息到控制台
	std::cout << "cppds.com" << endl;

	// 读取图像为灰度图像,图像路径为"lena_hed.jpg"
	auto img = imread("lena_hed.jpg", IMREAD_GRAYSCALE);

	// 获取图像的高度和宽度
	int height = img.rows;
	int width = img.cols;
	
	// 打印图像元素大小
	cout << "img.elemSize() = " << img.elemSize() << endl;
	
	// 显示原始图像
	imshow("test1", img);

	// 遍历图像的每一个像素
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			// 获取当前像素值
			auto c = img.data[i * width + j];
			
			// 将当前像素值进行反色操作,黑色(0)变为白色(255),白色(255)变为黑色(0)
			img.data[i * width + j] = 255 - c;
		}
	}
	
	// 显示反色后的图像
	imshow("test2", img);
	
	// 暂停等待5000ms(5秒),如果在这个期间有键盘输入则立即返回,如果没有则等待指定时间后返回
	waitKey(5000);
	
	return 0;
}
  • imread函数用于读取指定路径的图像,这里以灰度模式读取。
  • imshow函数用于显示图像,这里分别显示了原图和处理后的图像。
  • 通过img.data[i * width + j]访问到图像的每一个像素,然后进行反色处理。
  • waitKey函数则是使得程序暂停,等待用户输入,这里的5000表示等待5000ms,即5秒。如果在这个时间内,用户按下任何键,waitKey函数就会立即返回,如果超过这个时间,函数也会返回。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值