1 内存与地址
在说指针之前,肯定得说一说内存与地址了,因为指针的很多概念都与之相关.
内存是什么
从硬件形态上说,内存就是一条形物理设备,从功能上讲,内存是一个数据仓库,程序内在执行前都要被装载到内存中,才能被中央处理器(CPU)执行。
内存是由按顺序编号的一系列存储单元组成的,在内存中,每个存储单元都有唯一的地址,通过地址可以方便地在内存单元中存储信息。内存中的数据要靠供电来维持,当计算机关机或意外断电时,其中的所有数据就会永久地消失了
内存地址
内存地址指系统 RAM 中的特定位置,通常以十六进制的数字表示。内存地址是连续的,相邻内存单元间的地址差1,可以把内存看成一个平坦连续的一维空间。
可以将内存想象成一个个连续小格子的集合,为了正确地访问这些小格子,必须给这些小格子编号,正如平时我们讲某栋房屋在X小区A楼B单元M房间一样,这个X、A、B与M等实际上对应着该房间的编号,有了这个编号,或者更通俗地说是“地址”,我们就能从一个城市的万千栋长的几乎一样的房子中找到该房间。
内存中保存的内容
内存中每一个位置都保存着一个值。内存中保存的是数据,一切信息都是以二进制数据的形式体现的,每个内存单元的容量是1B,即8bit(8个0和1二进制位)。
变量与地址
变量是对程序中数据存储空间的抽象,每个变量基本上都有一个地址(寄存器变量无内存地址)
2 指针初窥
2.1 指针与指针变量
一般的,指针指的是一个变量的地址,指针变量指的是专门存放所指向的变量地址的变量。在不引起混淆的情况下,一般将指针变量简称为指针。
2.2指针与地址的区别
- 地址是一个标量,是不可改变的。只知道起始,不知结束。
- 指针是一个变量,存储的信息是某个内存单元的地址,指向了一块内存区域,并且指针是有大小的,在32位的情况下,指针变量的大小为4个字节,可用sizeof求出(64位的情况下为8个字节)。
2.3指针变量的声明与初始化
指针变量的声明
指针可以视为一个普通变量,通常所说的定义一个指针实际上是声明一个指针变量的过程,编译器根据指针变量声明语句,为指针变量开辟内存空间,使其有实际意义,这样指针变量才可用。
在声明一个指针变量时,需要向编译器提供以下信息:
1) 指针的类型 (原则上指针类型应与其指向的数据类型一致,但也有例外)。
2) 指针变量名。
如下形式:
指针变量的初始化
在声明一个指针后,编译器并不会自动完成其初始化,此时指针的值是不确定的,也就是说,该指针指向那块内存单元是完全随机的,因此,指针变量的初始化十分重要,直接使用未加初始化的指针变量可能会给程序带来各种内存错误,因为完全不知道哪块内存会被修改掉。
如果在指针变量声明之初确实不知道该将此指针指向何处,最简单的方式是将其置”0 “,C语言中提供了关键字NULL,如下:
int * p=NULL;
这样,指针p便不会在内存中乱指一气。
如果要让指针变量确切地指向某个变量,需要使用&取地址操作符。
示例:
int a = 10;
int *pa; // 声明一个指向整型的指针
pa = &a; // 使pa指向a
2.4 NULL指针
在标准中定义了NULL指针,是一个特殊的指针变量,值为0(类型为void*)。值为NULL的指针称为空指针,这意味着,指针并不指向任何地址(表示指针变量值没有意义)。
作用
1) 避免指针变量的非法引用
2) 在程序中常作为状态比较
注意:申明一个指针时,如果没有指向一个变量,最好将此指针赋值为NULL,防止产生野指针。同时,当指针为NULL时,对此指针进行间接访问(*操作)时,会触发访问异常错误。
2.5直接访问与间接访问
一般申明一个变量后,会通过&操作对指针赋值或将指针赋值为NULL。当我们需要访问指针所指向的内容的时候,可以使用间接访问操作符*。
区别
- 直接访问指按变量地址存取变量值
- 间接访问指的是通过存放变量地址的变量去访问变量。
示例:
int a = 10;
int *pa; // 声明一个指向整型的指针
pa = &a; // 使pa指向a
a = 20; // 直接访问
*pa = 30; // 间接访问
2.6指针变量作为函数参数与返回值
针变量作为函数参数时,传递的是地址,共享内存,“双向”传递。指针也可作为函数的返回值,但不可返回局部变量的地址(因为函数调用完毕后局部变量被销毁了)
注意:
1) 当形参是通过值传递的话,并不能改变实参的值
2) 当实参传递给形参的是一个地址,而形参是通过指针接收地址,间接访问来改变实参的值
指针变量作为返回值的错误案例
int* get(int* p)
{
int b = 20; // 局部变量,在栈上开辟,函数调用完毕就会被销毁
p = &b;
return p; // 返回了局部变量的地址
}
void main()
{
int a = 10;
int* pa = get(&a); // 使用指针p来接收返回的地址
printf("\n");
Sleep(5000);
printf("%d", *pa); // 打印出垃圾值
system("pause");
}
指针与一维数组
2.7.1 一维数组初始化与遍历
可以使用指针指向一个一维数组的首地址,数组名被编译器解析为数组的首地址。若指针p指向一个数组a,则有如下对应关系:
p+i等价于 &a[i] 或 *(p+i) 等价于 a[i]
a[i] 和 *(a+i)是 等价的
&a[i] 和 a+i 是等价的
示例:
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int* parr = arr; // parr指向数组arr首地址
for (int i = 0; i < 10; i++)
{
//printf("%d,%d\t", parr[i],arr[i]); // 等价
printf("%d,%d\t", *(parr+i),*(arr+i)); // 等价
}
2.7.2 数组做函数参数
数组做函数参数做函数的特点如下:
(1) 数组名既可以作形参也可以作实参。当使用数组做函数形参会自动退化为一个指针,指向数组的首地址,C编译都是将形参数组名作为指针变量来处理的。当数组名作实参时,是指针常量,指向数组的首地址。在函数执行期间,形参数组可以再被赋值
(2) 一维数组名可以作为函数参数,多维数组名也可作函数参数。
(3) 用指向数组的指针作函数形式参数以接受实参数组名传递来的地址。可以有两种方法:
- 1) 一维数组用指向变量的指针变量
- 2) 二维数组用指向一维数组的指针变量
示例:
void printArr1(int a[],int length) // 一维数组名做形参
{
for (int i = 0; i < length; i++)
{
printf("%d\t", a[i]);
}
printf("\n");
}
void printArr2(int a[3][4]) // 二维数组名做形参,至少知道列数(即参数4)
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d\t ", a[i][j]);
}
printf("\n");
}
printf("\n");
}
void printWithPointer1(int* p,int length) // 一级指针做形参,接收指针或数组首地址
{
for (int i = 0; i < length; i++)
{
printf("p[%d]=%d\t", p[i]);
}
printf("\n");
}
//一个指向每行有4个元素的行指针,用于接收一个二维数组a[][4]的首地址。
// 参数不要写作int** p,否则要么造成访问出错,要么需要进行类型强制转换(int(*p1)[4] = (int(*)[4])p)
void printWithPointer2(int(*p)[4])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("p[%d,%d]=%d\t ", i,j,p[i][j]);
}
printf("\n");
}
printf("\n");
}
void main1()
{
int a[5] = { 1,2,3,4,5 };
// printArr1(a, 5); // 数组名作实参
printWithPointer1(a, 5); // 数组名作实参
system("pause");
}
void main()
{
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,0,11,12} };
//printArr2(a);
printWithPointer2(a);
//int(*p)[4] = a; // 1
//int**p = a; // 2
//int(*pp)[4] = (int(*)[4])p; // 强制类型转换
//for (int i = 0; i < 3; i++)
//{
// for (int j = 0; j < 4; j++)
// {
// printf("%d,%d,%d\t ", i, j,*(*(pp+i)+j));
// }
// printf("\n");
//}
system("pause"); // 暂停
}
2.8指针的运算
作为一种特殊的变量,指针可以进行一些运算,但并非所有的运算都是合法的,指针的运算主要局限在加减算术和其他一些为数不多的特殊运算。
指针的加减运算
对于一般的指针而言,每加1表示地址增大所指向类型大小的长度。对于指向数组的指针而言,每加1则指针所指向的元素向后移动一位。
指针对数组的操作
1) 指针++就是按照指针类型的大小,前进一个类型的大小,如int 前进四个字节
2) 指针+n就是向前移动sizeof(指针类型)*n个字节,-n同理
3) 指针 ++ 和 -- 只有在数组的内部才有意义。
4) 指针的加减法在非数组内部没有任何意义,而且很容易越界报错
指针运算的优先级
1) *p++与*(p++)等价。同样优先级,结合方向为自右向左。
2) *(p++) 与*(++p) 前者是先引用再自增,先取*p的值,后使p值加1,相当于a[i++];后者是先先自增再引用,使p加1,再取*p,相当于a[++i]。
3) (*p)++表示p所指向的元素值加1,而非指针值加1
指针变量的赋值运算
p=&a; (将变量a地址赋值p)
p=array; (将数组array首地址赋值p)
p=&array[i]; (将数组元素地址赋值p)
p1=p2; (指针变量p2值赋值p1)
// 不能把一个整数赋值给p,也不能把p的值赋值整型变量
如
int i ,*p;
p=1000; // 错
i=p; // 错
指针之间的比较
- 1) 对两个毫无关联的指针比较大小是没有意义的,因为指针只代表了“位置”这么一个信息, 但是, 如果两个指针所指向的元素位于同一个数组(或同一块动态申请的内存中), 指针的大小比较反映了元素在数组中的先后关系。
- 2) 通过比较两个指针的地址,来判断哪个指针的位置考前还是靠后
地址的比较没有意义,只能判断谁的地址也就是内存的编号比较靠前
2.9指针的类型和指针所指向的类型
1) 所谓指针类型,指的是声明指针变量时位于变量名前的“类型*”,而所谓指针所指向的类型,指的是为指针初始化或赋值的变量类型。
2) 不是同一类型的指针,不可以任意赋值。不同的数据类型,大小不一样,解析方式不一样(如果强制赋值的话,就会少读取或多读取,内存有很多垃圾0,1)
3) 同类型指针的赋值
这是最常见的一种情况,如所示,pN1和pN2是两个相同类型的指针,执行“pN2=pN1;”这样一个赋值操作后,pN1和pN2指向同样的地址,也就是说,两个指针指向同一个内存单元,对*pN2的任何改动都会影响*pN1的值,反之亦然。
例:
char ch = 'a';
char* p1 = &ch;
int* p2 = p1;
char* p3 = p1; // 同类变量可以相互赋值
printf("%c,%d\n", *p1, *p2); // *p2的值通过%d打印出现垃圾值
*p3 = 'y';
printf("%c,%c\n", *p1, *p3);
//指针的类型必须要与指针指向的类型一致,不一致,大小不一样,解析方式不一样。
2.10 指针与const修饰符
const取自英文单词constant,是“恒定、不变”的意思,早期的C语言并没有const这个关键字,它限定一个变量不允许被改变。它取自C++而又有不同。
使用const在一定程度上可以提高程序的健壮性,防止数据被非法修改。
使用const的目的
通过在声明语句的不同位置使用const可达到3个目的:
- 1、禁止对指针重新赋值。
- 2、禁止通过间接引用(*指针)对指针所指的变量赋值。
- 3、既禁止对指针重新赋值,又禁止通过间接引用(*指针)对指针所指的变量赋值。
针对上面三个目的,指针与const修饰符配合使用时有三种形式:
常量指针
常量指针:指向的地址可以改变,但内容不可以重新赋值(不可通过指针间接赋值)。
语法如下:
const 数据类型 * 指针变量名;
指针常量
指针常量:指向的地址不可以改变,但内容可以改变(可通过指针间接赋值)
语法如下
数据类型 * const 指针变量名;
指向常量的常指针
指向的地址不可以改变,指向的内容也不可使用指针来间接改变。
语法如下
const 数据类型 * const 指针变量名;
const修饰指针与变量
int a = 5;
int b = 6;
int const *p1 = &a; //常量指针,指针可以改变指向,但不可间接改变指向的内容
int *const p2 = &b; //指针常量,指针不可改变它的指向
// *p1 = 6; // p1所指向的内容不可间接改变
p1 = &b; // p1可以改变指向
//p2 = &a; // p2是常量值,不可改变指向
*p2 = 10; // p2可以间接改变所引用的值
printf("%d,%d", *p1, *p2); // 打印
int i = 10;
const int* const pi = &i; // 指向常量的常指针
i = 20;
//*pi = 30; // 不可通过pi间接改变i的值
//pi = &b; // 不可改变pi的指向
做函数参数
void changep(const int* p)
{
// *p = 20; // 不可修改
}
void change(const int i)
{
// i = 100; // 不可修改
}
3 指针深入
3.1 二级指针
指针变量也是变量,占据一定的内存空间,也有内存地址,因此可以用一个指针指向它,这称为指向指针的指针,或二级指针。
可以通过 数据类型 ** 变量名; 来声明一个二级指针
示例:
int a=10;
int *pa = &a;
int **ppa = NULL; // 声明一个二级指针
ppa = &pa; // ppa指向pa
printf("%d,%p\n", a, &a);
printf("%d,%p,%p\n", *pa, pa,&pa);
printf("%d,%p,%p\n", **ppa,*ppa, ppa); //与上一行打印相同
3.2指针与函数参数
除了数组以外,传递的任何数据、变量,都会新建一个变量接收传入的变量的值。不影响原来的变量。如果是一个数据,应传递数据的地址(指针),如果是一个指针,传递指针的地址。
例
void change(int data) // 值传递,无法改变原变量的值
{
data = 99;
printf("changing a=%d\n", data);
}
void changeData(int* p) // 传递的是地址(指针),可以改变原变量的值
{
*p = 66; // 改变原变量的值
printf("changing a=%d\n", *p);
}
void changePointer(int* p) // 传递指针的值无法该变指针的指向
{
p = &b; // p是原指针的一份拷贝,改变它的指向无法改变原指针的指向
printf("changing a=%d\n", *p);
}
void changePointer2(int** p) // 传递指针的地址无法该变指针的指向
{
*p = &b; // 改变原指针的指向
printf("changing a=%d\n", **p);
}
3.3 N级指针
二级指针也是一个变量,可以被其他指针所指向。指向二级指针的指针称为三级指针。同理指向三级指针的指针为四级指针,…,指向N-1级指针的指针称为N级指针。
例:
int a = 6;
int* p = &a; // 一级指针,p指向a ,p存储a的地址
int** pp = &p; // 二级指针,pp指向p ,pp存储p的地址
int*** ppp = &pp; // 三级指针,ppp指向pp ,ppp存储pp的地址
int**** pppp = &ppp; // 四级指针,pppp指向ppp , ppppp存储ppp的地址
printf("%d,%d,%d,%d,%d\n", a, *p, **pp, ***ppp, ****pppp);
printf("%d,%#x\n", a, &a);
printf("%d,%#x\n", *p, p);
printf("%d,%#x\n", **pp, *pp);
printf("%d,%#x\n", ***ppp, **ppp);
printf("%d,%#x\n", ****pppp, ***pppp); // 对比结果
输出如下:
6,6,6,6,6
6,0x22fe44
6,0x22fe44
6,0x22fe44
6,0x22fe44
6,0x22fe44
3.4 指针与多维数组
3.4.1指针与二维数组
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
int(*p)[4] = a;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
// 等价关系
printf("%d,%d,%d,%d,%d,%d\n", p[i][j],a[i][j],*(*(p+i)+j), *(*(a + i) + j),*(a[i]+j), *(p[i] + j));
}
printf("\n");
}
区分 a、&a、*a
- a是一个行指针(int(*)[4])。指向一个有四个元素的数组,占16个字节
- &a是一个指向二维数组的指针(int(*)[3][4]),二维数组有12个元素,占48个字节
- *a是一个指向int类型数据的指针(int*)。
C语言可以通过定义行数组指针的方法,使得一个指针变量与二维数组名具有相同的性质。行数组指针的定义方法如下:
数据类型 (*指针变量名) [二维数组列数];
例如,对上述a数组,行数组指针定义如下:
int (*p) [4];
说明:
定义int(*p)[4];则定义了一个名为p的指针变量,它指向每行有四个int型元素的二维数组 . p是指向一维数组的指针变量(行指针),也就是p这个指针是指向一个含有4个元素的一维数组,那么p指针每一次加1就相当于把p中存的地址加16(一个包含4个int的数组的地址长度,4*sizeof(int),此处int类型占4个字节)。
3.4.2 指针与三维数组
示例:
int a[3][4][5];
for (int* p = &a[0][0][0],i=0; p < &a[0][0][0] +60; p++,i++)
{
*p = i; // 初始化
}
//int(*p)[4][5] = a; // 使用printf("%d\t", p[i][j][k]);来打印
for (int i = 0; i < 3; i++) // 打印
{
printf("\n第%d页\n", i);
for (int j = 0; j < 4; j++)
{
for (int k = 0; k < 5; k++)
{
// 等价关系
printf("%d,%d,%d,%d\t\t", a[i][j][k], *(*(*(a+i)+j)+k),*(*(a[i]+j)+k),*(a[i][j]+k));
}
printf("\n");
}
}
3.4.3 指针与四维数组
int a[3][4][5][6];
for (int*p=&a[0][0][0][0], i = 0; p < &a[0][0][0][0]+ 360; p++,i++) // 初始化
{
*p = i;
}
for (int i = 0; i < 3; i++) // 打印
{
printf("\n\n第%d三维\n\n",i);
for (int j = 0; j < 4; j++)
{
printf("\n第%d二维\n",j);
for (int k = 0; k < 5; k++)
{
for (int l = 0; l < 6; l++)
{
printf("%d,%d\t", a[i][j][k][l], *(*(*(*(a + i) + j) + k) + l));
}
printf("\n");
}
}
}
3.4.4 总结
对于数组与指针的等价关系总结如下:
二维:a[i][j] , *(*(a + i) + j) , *(a[i]+j)
三维:a[i][j][k], *(*(*(a+i)+j)+k), *(*(a[i]+j)+k), *(a[i][j]+k)
四位:a[i][j][k][l], *(*(*(*(a + i) + j) + k) + l), *(*(*(a[i] + j) + k) + l), *(*(a[i][j] + k) + l), *(a[i][j][k] + l)
...
更多维度一次类推
3.5 指针数组
指针数组即创建的一个数组,每个元素都是一个指针变量。
示例:
int _sub(int a, int b)
{
return a - b;
}
int _add(int a, int b)
{
return a + b;
}
void main()
{
char s1[20] = "hello";
char s2[20] = "world";
char s3[20] = "pecuyu";
char* str[3] = {s1,s2,s3 }; // 字符串指针数组,每个元素都指向一个字符串的首地址
for (int i = 0; i < 3; i++)
{
printf("%s\t", str[i]);
// strcpy(str[i], "niko"); // 修改指向的字符串的内容
}
int(*p[2])(int, int) = { _sub,_add }; // 函数指针数组,每个元素都是函数指针
for (int i = 0; i < 2; i++)
{
int r= p[i](1, 2);
printf("%d\t", r);
}
system("pause");
}
指针数组与二维数组的区别:
- 1) 指针数组元素的作用相当于二维数组的行名,指针数组中元素是指针变量,对于char* str[2] = { {“hello”},{“world”} }; 对其内容进行修改是不合法的,如strcpy(str[1],”niko”);操作非法,但可改变其指向,如str[0]=str[1];操作合法
- 2) 二维数组的行名是地址常量,如对char str[2][10] = { {“hello”},{“world”} }; 操作str[0]=str[1]; 是非法的,因为str[0]指示的是字符串常量”hello”的地址,故不可被修改。但str[0][0]=’q’;是可以的,因为str[0][0]代表的是二维数组的第一个元素
区别int* p[2] 与 int(*p)[2]
- int* p[2] 是包含有两个指向整型的指针(int*) 元素的数组
- int(p)[2] 是行指针,指向包含两个整型(int [2])的数组。注意优先级 [] 比 大。
4 函数指针
4.1定义
如果在程序中定义了一个函数,在编译时,编译系统为函数代码分配一段存储空间,这段存储空间的起始地址,称为这个函数的指针。
可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。例如:
int (*p)(int,int);
定义p是指向函数的指针变量,它可以指向类型为整型且有两个整型参数的函数。p的类型用int (*)(int,int)表示
4.2内存原理
函数被载入内存,函数必然有一个地址是函数的入口,我们用这个地址来调用函数,函数名也是指向函数入口点的指针,我们可以通过函数名找到函数的执行入口。
同时C语言的编译器(VC或者GCC)都有如下的规则:
针对函数void go(),函数名go解析为函数的地址,go ,& go ,* go 都解析为go函数的入口地址,即为go函数的首地址。因此通过(*go)(); 、go(); 、(&go)(); 来调用函数都是合法的。而且函数名不可以用sizeof操作符。
4.3函数指针的作用
- 1) 指向函数的指针变量的一个重要用途是把函数的地址作为实际参数传递到其他函数
- 2) 指向函数的指针可以作为函数形式参数,接收函数调用时传入的实际参数(函数地址),这样就能够在函数中调用实参函数
对第2条的一个简单应用
#include<stdio.h>
#include<stdlib.h>
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
int ops(int a,int b,int(*p)(int,int)) // p为函数指针,做函数参数,可实现不同功能
{
return p(a,b);
}
void main()
{
int a=3,b=4;
//int result=ops(a,b,add); // 调用add来运算
int result=ops(a,b,sub); // 调用sub来运算
printf("result=%d",result);
}
4.4函数指针的定义
定义函数指针
定义指向函数的指针变量的一般形式为
数据类型 (*指针变量名)(函数参数表列);
int (*p)(int,int); // 定义一个参数为(int,int),返回值为int的函数指针
p=add; // 对
// p=add(a,b); 错
// p+n,p++,p--等运算无意义函数是代码扩充会发生变化
函数返回值
一个函数可以返回一个整型值、字符值、实型值等,也可以返回指针型的数据,即地址。其概念与以前类似,只是返回的值的类型是指针类型而已
定义返回指针值的函数的一般形式为
类型名 *函数名(参数表列);
定义返回值为函数指针值的函数的一般形式为
类型名 (* 函数名( 参数表列) )(返回值参数列表)
4.5 使用typedef定义指针函数
#define 与 typedef的区别
#define是种字面上的简单替换,而typedef却是引入一个新的助记符号(别名)。
typedef需要分号,#define一般不能加上分号,加上分号会连带分号一起替换
简单案例看出区别
typedef int* pint;
#define PINT int*
void main()
{
PINT pi1, p2; // 两者类型不同,前者pi1是int* , 后者p2是int,因为#define只是做了简单的替换
pint pint1, pint2; // 两者类型都是pint,即int*
}
使用typedef定义指针函数
typedef int(*pFun)(int, int) ; // 定义了一个指向参数为(int, int),返回值为int的函数指针
int add(int a, int b)
{
return a + b;
}
void main()
{
pFun fun=sub; // 此时pFun是类型名
int r= fun(1, 2);
printf("%d", r);
system("pause");
}
4.6 函数指针的使用示例
例1 函数指针的基本使用
#include<stdio.h>
#include<stdlib.h>
int sub(int a, int b)
{
return a - b;
}
int add(int a, int b)
{
return a + b;
}
void main()
{
int(*p)(int, int); // 定义一个函数指针
p = add; // 指向add函数
int r = p(1, 2); // 发起调用
printf("%d\n",r);
p = sub; // 指向sub函数
r = p(3, 5);
printf("%d\n", r);
system("pause");
}
例2 函数指针做参数与返回值
int(* _change( int(**p)(int, int) ) )(int, int) // 函数指针地址做参数,函数指针做返回值,参水类型int(** )(int, int) , 返回值类型 int(*)(int, int)
{
*p = &add; // 此处p的指向实际已经改变,为了演示才添加了返回值
return *p;
}
void main()
{
int(*p)(int, int);
p = sub;
int r = p(1, 2); // 发起调用
printf("%d\n", r);
p = _change(&p);
r = p(1, 2); // 发起调用
printf("%d\n", r);
system("pause");
}
5指针与动态内存分配
动态内存分配,是指用户可以在程序运行期间根据需要申请或释放内存,大小也完全可控。动态分配不像数组内存那样需要预先分配空间,而是由系统根据程序需要动态分配,大小完全按照用户的要求来,当使用完毕后,用户还可释放所申请的动态内存,由系统回收,以备他用。
5.1 malloc 函数
malloc函数是C标准库中提供的函数,用以动态申请内存,malloc()函数的原型为:
void *malloc( unsigned int size );
参数:
- 1) 参数size是个无符号整型数,用户由此控制申请内存的大小,执行成功时,系统会为程序开辟一块大小为size个内存字节的区域,并将该区域的首地址返回,用户可利用该地址管理并使用该块内存,如果申请失败(比如内存大小不够用),返回空指针NULL,此时需作判空处理。
- 2) malloc()函数返回类型是void*,用其返回值对其他类型指针赋值时,必须进行显式转换。size仅仅是申请字节的大小,并不管申请的内存块中存储的数据类型,因此,申请内存的长度须由程序员通过“长度×sizeof(类型)”的方式给出,举例来说:
int* p=(int*) malloc(10* sizeof(int) ); // 申请了10*4个字节的内存
注意:
- 1) 使用malloc 函数进行分配内存时可能失败而返回NULL,因此有必要作判空处理
- 2) 对分配的内存进行操作时不要越过边界,否则可能会造成非法访问等异常
5.2 free 函数
free函数是C标准库中提供的函数,用以释放内存。函数原型如下
void free(void *_Memory);
通过malloc等函数在堆上申请的内存,最终需要通过free函数来释放内存,例如free(p)。当向free传递NULL指针时,无任何效果(free(NULL);)。因此,当释放内存后,可以显式的将被释放的指针置NULL。
注意:
- 1) 不要释放非动态分配的内存。
- 2) 不要在已经释放的内存区域继续使用它
- 3) 不要重复释放已经被释放的内存
5.3 calloc函数
calloc函数与malloc函数作用大致相同,都用于动态申请内存。不同的是,calloc函数会将申请的内存自动清零。函数原型如下
void *calloc(size_t _NumOfElements,size_t _SizeOfElements);
_NumOfElements表示要申请多少个元素,_SizeOfElements表示要申请的每个元素大小为多大。如果分配内存需要初始化,则会带来便利,但程序若是只是将数值存入到数组中,则使用calloc时的初始化则只是浪费时间。
5.4 realloc函数
realloc函数用于重新分配(用malloc或者calloc函数)在堆中已分配好的内存空间的大小。函数原型如下:
void *realloc(void *_Memory,size_t _NewSize);
第一个参数 _Memory是之前用 malloc或者calloc 动态分配的内存的首地址,_NewSize为重新分配内存的大小,单位:字节。
使用此函数可以扩大或缩小分配的内存空间,这取决于你传入的_NewSize(_Block* _Size)大小与原来的分配内存的大小关系。有如下情况:
(1) 当_NewSize 大于原分配的内存大小时,若原分配内存后面能够有足够内存空间分配,则会直接分配到当前内存的后面;若原先的内存无法拓展以满足要求,则会重新申请一块内存,并把原有内存的内容拷贝到新的内存区域,此时指向内存首地址的指针发生变化,应该用realloc函数返回的新指针,原内存区域被释放。
(2) 当_NewSize 小于原分配的内存大小时,则会直接从原有内存的某个部分截断,丢弃大于申请的大小的部分,保留的部分仍然是原有的内容。
(3) 当realloc申请内存失败,则会返回NULL,因此判空是有必要的。
注意事项:
- 1) 成功返回新分配的堆内存地址,失败返回NULL.
- 2) 如果参数_Memory等于NULL,那么realloc与malloc功能一致
- 3) realloc函数不会对新分配的内存进行初始化
- 4) 应该始终使用返回的新指针,因为返回的新指针和之前的指针可能是不一致的,若继续使用原指针将导致错误。
5.5 内存泄漏
当我们动态分配的内存不再使用时,我们应该释放它,以便可以被重新分配并使用。若分配的内存在我们使用完后不需要时没有被释放,则会引起内存泄漏(memory leak)。内存泄漏可能会导致可用内存越来越少,使得程序运行变慢,甚至是系统崩溃。因此防止内存泄漏是极其重要的。
示例
// 检查分配内存是否成功
void checkMemoryState(void* p)
{
if (p == NULL)
{
printf("\n分配内存出错\n");
exit(0);
}
}
void main()
{
int *a = (int *)malloc(sizeof(int) * 10);
checkMemoryState(a); // 检查分配内存是否成功
for (int i = 0; i < 10; i++)
{
a[i] = i+1; // 初始化
}
for (int i = 0; i < 10; i++)
{
printf("%d\t", a[i]); // 打印
}
a = realloc(a, 15*sizeof(int)); // realloc不会对新分配的内存进行初始化
checkMemoryState(a);
memset(a + 10, 0, sizeof(int)*5); // 对后申请的5个元素进行初始化
for (int i = 0; i < 15; i++)
{
printf("%d\t", a[i]); // 打印
}
free(a); // 释放内存
// free(a); // 不要重复释放已经被释放的内存
//free(NULL); // 传递空时,无任何效果
system("pause");
}
6 指针与结构体
相关内容参考C语言结构体与共用体中的 结构体与指针相关内容。
后记:指针的内容是多而且复杂的,几乎所有的其他知识都能与其产生关系,因此,需要更多的实战才能把它掌握透彻。欢迎指错,欢迎交流学习~