指针
指针是什么
数据在内存中存放的方式
声明一个变量int i = 3;
,那么在内存中就会分配一个大小为4字节(因为int类型占4字节)的内存空间给变量i
,这块内存空间存放的数据就是变量i
的值。
换句话说就是,在内存中给变量i
分配了一个房间,房间号为2000,这个房间号就是变量i
在内存中的地址。
数据的读取
每次访问变量i
其实就是到 2000 这个房间去把里面的数据读出来,比如:
printf("%d\n", i);
实际上是通过变量名i
找到存储单元的地址,从而对存储单元进行存取操作。程序经过编译以后,已经把变量名转换为变量的地址,**对变量值的存取都是通过地址进行的。**再比如:
scanf("%d", &i);
这里的&
就是取地址符,&i
的值就等于变量i
的地址 2000 ,这条语句就是要把键盘输入的数据存到内存中首地址为2000的整型存储单元。即从2000开始往后的4个字节。
以上这种直接根据变量名进行访问的方式,称为“直接访问”。
指针变量
与之相对应的就有“间接访问”的方式。即将变零i
的地址值存放到另一个变量中,然后通过访问这个变量的值来找到变量i
的地址,从而访问变量i
。这个存放地址的变量就叫做指针变量。比如:
int *i_pointer = &i;
这里就声明了一个名为i_pointer
的指针变量,他的值就是变量i
的地址2000。
注意:
- 指针变量:是存储地址的一个变量,即
i_pointer
- 变零的指针:是指该变量的地址,即
i
的地址2000
定义指针变量
一般形式为:类型名 指针变量名,如:int *p1;
就是声明了一个指向整型数据*的指针变量p1
。
其中int
表示该指针变量的基类型,他用来指定该指针变量可以指向的变量的类型。比如int *p1;
这里的指针变量p
,就只能指向int
类型的变量,而不能是char
类型的。原因如下:
不同类型的变量在内存中分配的空间大小是不同的,int
是4字节,char
是1字节。指针变量指向的地址表示的是该变量的存储单元的首地址,比如上面提到的变量i
的地址是2000,其实2000、2001、2002、2003这4个字节的空间都是归变量i
所有的。当我们拿着2000这个地址要去读数据的时候,就必须知道这个数据是什么类型,我们才知道要往后读多少个字节。
**注意:指针变量只能存放地址,不要将一个整数赋值给一个指针变量。**比如:
*pointer = 100;
引用指针变量
在引用指针变量的时候可能有以下三种情况:
-
给指针变量赋值,如:
int *p; p = &i; //把变量i的地址赋给指针变量p
-
引用指针变量指向的变量
printf("%d", *p); //以整数形式输出指针变量p所指向的变量的值,即变量i的值 *p = 10; //表示把指针变量p所指向的变量i的值设为10,即i = 10,这种方式叫做指针的解引用
-
引用指针变量的值
printf("%o", p); //以八进制数形式输出指针变量p的值,即输出变量i的地址,即&i
注意:
&
:取地址运算符,用来得到一个变量的地址值*
:指针运算符,*p
表示的是指针变量p
指向地变量
指针变量作为函数参数
可以通过指针变量做为一个函数的参数,将某个变量的地址,传入到另一个函数中。
普通变量在作为函数参数时,传递的是该变量的一个副本,即在下一个函数中的变量与传递进来的变量根本不是在同一个内存空间,只不过是将上一个该变量的值进行了拷贝传递进来,在函数内部如果改变了该变量的值,在外部是感受不到的。举个栗子:
void swap(int x, int y){
int temp = x;
x = y;
y = temp;
}
void main(){
int a = 5;
int b = 10;
swap(a, b);
printf("a = %d, b = %d", a, b);
}
上面这种方式swap()
函数内部进行了变量x
与变量y
的交换,但是在main()
函数中的a
与b
是没有改变的,因为在函数调用的时候,是把变量a
,b
的值分别赋值给了新的变量x
,y
,这两个变量在内存中分别有自己的空间,互不影响。
所以要将上面的代码的改为下面这样:
void swap(int *p1, int *p2){
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void main(){
int a = 5;
int b = 10;
swap(&a, &b); //传入a,b的地址
printf("a = %d, b = %d", a, b);
}
指针与数组
指针与一维数组
int a[5] = {0}; //声明一个长度为5的整型数组
数组名a
表示的是数组在内存中的首地址,即a[0]
的地址。
int *p = a; //数组名a表示数组的首地址
int *p = &a[0]; //数组a首元素的地址
以上这两条语句是一样的效果,即将数组a
的首地址赋给指针变量p
(注意不是*p
)。
在引用数组元素时指针的运算
在指针指向数组元素时,可以进行以下运算:
- 加/减一个整数,如
p + 1
,表示该数组中的下一个元素 - 自加/自减运算,如
p++
,表示指向该数组中的下一个元素 - 两个指针相减,如
p2 - p1
,表示p2
所指元素与p1
所指元素之间相差几个元素。- 注意只有当
p1
和p2
都指向同一个数组中的元素时才有意义; - 两个地址不能相加,比如
p1 + p2
没有实际意义
- 注意只有当
通过指针引用数组元素
-
下标法
我们可以通过下标来访问数组元素,也可以通过指针来访问。
a[i]
,访问的就是从数组首地址a[0]
开始的第i
个元素。 -
指针法
那么通过指针来访问就是
*(a + i)
,这里的a
就是数组首地址,再加上i
个存储单元,那么a+i
,就是数组中从0开始,第i
个元素的内存地址,最后用*
表示指针的解引用,得到的就是a[0+i]
的元素值。注意:这里的
a + i
得到的是一个地址值,并且这里的i
表示的不是i
个字节数,而是**i
个数组元素所占的字节数**。比如数组a
中存储的是int
类型的变量,那么a + i
就是首地址a
加上i*4
个字节。因此
a[i]
等价于*(a + i)
,等价于p[i]
,等价于*(p + i)
指针与多维数组
多维数组元素的地址
在C语言中,理解多维数组元素的地址是理解多维数组内存布局的关键。多维数组在内存中是连续存储的,这意味着无论数组有多少维,其元素都会被放置在一块连续的内存区域中。了解如何计算多维数组元素的地址可以帮助你更好地理解数组是如何工作的,以及如何通过指针访问数组元素。
一维数组的地址
在讨论多维数组之前,先简单回顾一下一维数组。假设有一个一维数组arr
,其元素类型为T
(可以是int
、float
等),数组的第i
个元素(从0开始计数)的地址可以通过以下方式计算:
&arr[i] = 基地址 + i * sizeof(T)
其中基地址
是数组首元素arr[0]
的地址,sizeof(T)
是数组元素类型的大小。
二维数组的地址
对于二维数组,情况稍微复杂一些。假设有一个二维数组定义为T arr[M][N]
,其中M
是行数,N
是列数。数组的某个元素arr[i][j]
(其中i
表示行索引,j
表示列索引)的地址可以通过以下方式计算:
&arr[i][j] = 基地址 + (i * N + j) * sizeof(T)
这里,(i * N + j)
计算的是元素arr[i][j]
在按行展开的一维数组中的索引位置,然后乘以类型T
的大小sizeof(T)
得到从基地址开始的偏移量。
多维数组的地址
对于更高维度的数组,地址的计算方式遵循相似的逻辑。以三维数组T arr[X][Y][Z]
为例,某个元素arr[i][j][k]
的地址可以通过以下方式计算:
&arr[i][j][k] = 基地址 + ((i * Y + j) * Z + k) * sizeof(T)
这里,((i * Y + j) * Z + k)
计算的是元素arr[i][j][k]
在按行、面展开的一维数组中的索引位置。
总结
- 多维数组在内存中是连续存储的。
- 无论数组有多少维,其元素的地址计算都遵循将多维索引转换为一维索引的逻辑,然后根据一维索引计算地址。
- 理解多维数组元素地址的计算方法有助于深入理解数组的内存布局,以及如何通过指针操作数组元素。
通过掌握这些概念,你将能够更加灵活和高效地使用C语言中的数组和指针。
动态分配内存
C语言的内存映像
在C语言中,动态内存分配是一个非常重要的概念,它允许程序在运行时根据需要分配和释放内存。这种能力使得程序可以更加灵活地处理数据,尤其是在处理数据大小未知或数据大小会变化的情况下。C语言提供了几个标准库函数来支持动态内存管理,主要包括malloc
、calloc
、realloc
和free
。
malloc
malloc
函数用于分配一块指定大小的内存区域。它的原型定义在stdlib.h
头文件中,其基本语法如下:
void* malloc(size_t size);
size
:要分配的内存大小(以字节为单位)。- 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回NULL。
分配的内存是未初始化的,可能包含任意数据。
calloc
calloc
函数类似于malloc
,但有两个不同之处:一是它可以分配多个连续的对象;二是分配的内存会自动初始化为零。其原型也定义在stdlib.h
中,基本语法如下:
void* calloc(size_t num, size_t size);
num
:要分配的元素个数。size
:每个元素的大小(以字节为单位)。- 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回NULL。
realloc
realloc
函数用于重新调整之前分配的内存块的大小。这可以用于扩大或缩小内存块。它的原型定义在stdlib.h
中,基本语法如下:
void* realloc(void* ptr, size_t size);
ptr
:指向先前由malloc
、calloc
或realloc
分配的内存块的指针。size
:新的内存块大小(以字节为单位)。- 返回值:如果重新分配成功,返回指向新内存的指针;如果失败,返回NULL。
如果size
为0,则realloc
会释放ptr
指向的内存,并返回NULL。
free
free
函数用于释放之前通过malloc
、calloc
或realloc
分配的内存。释放后的内存不能再被访问,否则会导致未定义行为。free
的原型定义在stdlib.h
中,基本语法如下:
void free(void* ptr);
ptr
:指向先前分配的内存块的指针。
使用动态内存时,必须确保最终释放分配的内存,以避免内存泄露。正确管理动态内存是编写健壮、高效C程序的关键之一。
示例
以下是一个使用malloc
和free
的简单示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*) malloc(sizeof(int)); // 分配一个整型变量的内存
if (ptr == NULL) {
printf("内存分配失败\n");
return 1;
}
*ptr = 10; // 使用分配的内存
printf("%d\n", *ptr);
free(ptr); // 释放内存
return 0;
}
这个例子展示了如何动态分配一个整数的内存,使用它,然后释放它。正确地使用动态内存分配和释放对于避免内存泄露和保证程序稳定性至关重要。
动态分配内存实现二维数组
在C语言中,通过动态分配内存来创建二维数组需要使用指针和malloc()
或calloc()
函数。这种方法提供了更多的灵活性,特别是当你事先不知道数组大小时。
使用一级指针模拟二维数组
在C语言中,可以使用一级指针来模拟二维数组的行为。这种方法通常涉及到计算索引以访问内存中连续存储的数据。这样做的一个好处是,你可以动态地根据需要分配和调整内存的大小,这在处理变长的数据结构时非常有用。
要使用一级指针模拟二维数组,你首先需要确定每个维度的大小。假设我们想模拟一个rows x cols
的二维数组。接下来,我们分配足够的连续内存来存储所有元素,即rows * cols
个元素的空间。然后,通过适当的索引计算,我们可以像访问二维数组那样访问这些元素。
动态分配内存
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 5;
int cols = 4;
int *array;
// 分配内存
array = (int *)malloc(rows * cols * sizeof(int));
if (array == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
// 初始化数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
*(array + i * cols + j) = i * cols + j; // 计算索引并赋值
}
}
// 打印数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", *(array + i * cols + j));
}
printf("\n");
}
// 释放内存
free(array);
return 0;
}
关键点
- 内存分配:使用
malloc
根据二维数组的总大小(rows * cols
)分配足够的内存。 - 索引计算:通过
*(array + i * cols + j)
访问元素,其中i
是行索引,j
是列索引。这里,我们将二维数组的索引映射到一维数组的索引上。 - 内存释放:使用完数组后,不要忘记使用
free
释放分配的内存,避免内存泄漏。
总结
通过一级指针和适当的索引计算,我们可以在C语言中模拟二维数组的功能。这种方法提供了更多的灵活性,特别是在处理动态数据结构时。然而,它也要求程序员更加小心地管理内存,包括正确地分配和释放内存,以避免内存泄漏或其他内存相关的错误。
使用二级指针分配二维数组
在C语言中,使用二级指针分配二维数组是一种模拟真正的二维数组行为的方法,同时提供了动态内存管理的灵活性。这种方法涉及到动态地为每一行分配内存,然后用一个指针数组(即二级指针)来管理这些行。这样做的好处是可以处理不同长度的行,从而创建“不规则”的二维数组。
步骤
- 分配指针数组:首先,你需要分配一个指针数组,其中每个指针将用于指向一行。
- 为每行分配内存:接着,对于指针数组中的每个指针,分别分配足够的内存以存储相应行的数据。
- 使用二维数组:一旦分配了内存,就可以像使用常规二维数组那样使用它,通过二级指针访问元素。
- 释放内存:使用完毕后,需要首先释放每一行的内存,然后释放存储行指针的数组。
示例代码
下面是一个具体的例子,演示如何使用二级指针动态分配二维数组,并初始化、使用和释放这个数组:
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 5; // 行数
int cols = 4; // 列数
int **array;
// 第1步:为行指针分配内存
array = (int **)malloc(rows * sizeof(int *));
if (array == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
// 第2步:为每行分配内存
for (int i = 0; i < rows; ++i) {
array[i] = (int *)malloc(cols * sizeof(int));
if (array[i] == NULL) {
fprintf(stderr, "内存分配失败\n");
// 出错时,释放之前已分配的内存
for (int j = 0; j < i; ++j) {
free(array[j]);
}
free(array);
return 1;
}
}
// 第3步:使用二维数组
// 初始化二维数组
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
array[i][j] = i * cols + j;
}
}
// 打印二维数组
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
printf("%d ", array[i][j]);
}
printf("\n");
}
// 第4步:释放内存
for (int i = 0; i < rows; ++i) {
free(array[i]); // 释放每行的内存
}
free(array); // 释放行指针数组的内存
return 0;
}
关键点
- 使用二级指针分配二维数组提供了更多的灵活性,特别是对于行长度可能不同的情况。
- 动态分配内存时,务必记得最后释放内存,以避免内存泄露。
- 在为每行分配内存时,如果任何一次
malloc
调用失败,应该先释放之前已经成功分配的内存,然后退出程序。这是良好的错误处理实践。
通过这种方式,你可以灵活地处理各种大小和形状的二维数据结构,使你的C语言程序能够更有效地处理动态数据。