文章目录
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 堆空间
堆空间是由程序员负责管理的,我们可以使用 new
和 delete
在堆上分配和释放内存。堆空间的数组同样可以使用 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 = #
在上面的代码中,我们创建了一个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
函数就会立即返回,如果超过这个时间,函数也会返回。