填坑与复习C语言中指针的各个知识点,
本文包含多个实例,具体请点击目录索引查看
一、指针变量的定义与使用
定义时:
- 基类型 * 指针变量名
- int * x;
- float * y;
使用时:
- *x代表的是指针变量x所指向的内存单元里的内容(实际值)
- x代表指向该内存单元的地址
- 指针变量必须要初始化,若不知指向何地址,指定为NULL(前提# include <stdio.h>,包含NULL的宏定义)
- 假设int *j = NULL,NULL指向的是内存中地址为0的内存空间,而在操作系统中,该内存单元是不可用的
- 切记:不要往存放NULL地址的指针变量里写入数据
以下为错误程序:
# include <stdio.h>
int main(void)
{
int i = 5;
int *j = NULL;
*j = i;
printf("hello\n");
printf("j=%d\n",j);
return 0;
}
你会发现,编译,链接,都不会报错,就执行时候,根本不会打印hello。(科科)
初始化为NULL,与初始化int 为 0 道理差不多,只要将该指针变量再指向其它有意义的可用地址就可以了。
在使用时:
- x代表指针变量(地址值)
- * x代表指针变量x里面存放的地址 所指向的存储单元的数据(实际值)
(1)两个指针变量之间仅支持相减运算
(在同一块内存空间)两个指针变量相减,是一个常量
int *a,*b,*c; // a,b,c皆为指针变量
int j,k;
int p=3,q=45,r=6789;
a = &p; // a,b,c分别指向p,q,r的内存单元(地址)
b = &q;
c = &r;
j = a-b;
k = a-c;
printf("a=%d\n",a);
printf("b=%d\n",b);
printf("c=%d\n",c);
printf("j=%d\n",j);
printf("k=%d\n",k);
结果为:
j = 1 代表相差 1 个元素。
为什么?
- 不同类型在内存中所占字节数是不同的,(如int占4B,char占1B),每个字节都有一个地址,而指针变量仅存放其所指变量的首地址。
- 一个int型的变量占4字节,所以一个int元素占4字节,a,b两个地址之间差4个地址,正好是1个int元素。
- 该特点,在数组中取地址更为明显
(2)指针变量仅支持加减运算。
写个小程序吧:从键盘输入10个数整数,输出最大偶数(使用指针访问数组)
if (flag) 的写法还是比较有意思的,flag改成bool型也可
# include <stdio.h>
int main(void)
{
int a[10] ={0};
int *p = a; // a代表该数组首地址
int max;
int i; // 循环变量
int flag=1; // 标志位
printf("输入十个整数\n");
for(i=0; i<10; i++)
{
scanf("%d", p+i); // 指针变量p 后移1个元素
}
for(; p<a+10; p++)
{
if(*p % 2 == 0) // 找到偶数
{
if(flag) // 第一个偶数赋给max(仅执行最多 1 遍)
{
max = *p;
flag = 0;
}
else if(*p > max)
{
max = *p;
}
}
}
if(!flag)
{
printf("最大偶数为:%d\n", max);
}
else
{
printf("未找到偶数\n"); // flag一直为1,数组里压根没偶数元素
}
return 0;
}
既然p可以p++,那么数组a首地址可不可以a++?
No,不可以。p是指针变量,a是数组首地址值,为常量。
只有变量可以++/--,常量不行!
(3)指针作为函数参数
以直接操作内存单元的方式来传参。下面以两数交换作为示例:
#include <stdio.h>
void Swap(int *,int *); //声明一个交换函数
int main()
{
int a = 3;
int b = 5;
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("a地址为%d\n", &a);
printf("b地址为%d\n", &b);
Swap(&a, &b); //对a,b取地址
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("a地址为%d\n", &a);
printf("b地址为%d\n", &b);
return 0;
}
void Swap(int *p, int *q)
{
int buf;
buf = *p;
*p = *q;
*q = buf;
return;
}
成功地交换了俩数实际值。
二、指针操作数组
(1)指针、数组、函数综合练习
写一个函数,实现从标准输入流提取字符串的功能:
- 模仿gets()与 fgets()函数源码
- 利用指针变量操作字符数组,利用*(p+i) 访问数组的各个元素
- 注意添加字符串结束标志
/*
从标准输入流读取字符串
*/
#include <stdio.h>
char * MyGets(char *, int); // 函数声明
int main(void)
{
char str[10]={0}; //申请10个字符大小的数组(实际仅能放8个字符)
printf("请输入字符串:");
MyGets(str,10);
printf("%s",str); // 无需"\n"
return 0;
}
char * MyGets(char *p, int n) // 形参为数组首地址
{
int i=0;
*(p+n-1) = '\0'; // 字符串最后一位空间为标志符'\0'
*(p+n-2) = '\n'; // 换行符放结束标志'\0'之前
for(i=0; i<n-2; i++)
{
*(p+i) = getchar();
if( *(p+i) == '\n') // 若读到了换行符,则输入字符数少于n-2
{
for(i++; i<n-1; i++)
{
*(p+i) = '\0'; // 保存换行符,并将其后所有元素置为'\0'
}
}
}
return p;
}
倒数第二位为 '\n'换行符; 最后一位为 '\0'结束符
(2)用const修饰指针变量
const修饰的变量为:常变量 --> 有名字的不变量--->只读变量--->易维护--->可取代“宏”
- 生命周期:程序运行的整个阶段
- 存储区域:“只读数据段”(跟常量/只读变量一起)
- 以下三种效果:
1. const int *p = &a;
- 此时const 与 int可互换位置。
- const修饰的是 *p
- *p不可变,其它的都可变。
- *p代表的是指针变量p所指向的内存单元里的内容(实际值)
- p代表指向该内存单元的地址
- 那么,p可变,指向可变。但指向谁,谁的内容不可变。
- 常用于定义函数的形参,将形参定义为只读的,避免数据“暴露”。
2.int * const p = &a;
- const修饰的是 p
- p地址值不可变
- *p实际值可变
- 即p的指向不可变,p所指向的内存单元的内容可变。
3.const int * const p = &a;
- const修饰的是 *p 和 p
- p和*p都不可变。
三、动态内存
(1)实现数组的动态分配大小
- 要求:使用指针访问数组元素
- 使用malloc( ) 函数与 realloc( ) (# include <stdlib.h>)
- void *malloc (unsigned long size);-----> 分配size大小的内存(堆中)----> 返回void * 型的地址
- void *realloc (void *p, unsigned long size) ---> 定义新长度
/*
实现数组的动态分配大小
*/
# include <stdio.h>
# include <stdlib.h>
int main(void)
{
int cnt1, cnt2;
int *p; //指针指向数组首地址
int i;
printf("请输入数组存放元素个数:");
scanf("%d", &cnt1);
p =(int *)malloc(sizeof(*p) * cnt1); //初始数组所占空间大小
// 扩展到的目标大小
printf("请输入新元素个数:");
scanf("%d", &cnt2);
realloc(p, sizeof(int) * cnt2);
// 输入
printf("请输入数组内容,以空格分隔:\n");
for(i=0; i<cnt2; i++)
{
scanf("%d", p+i); // p+i 等价于 &p[i]
}
// 输出
printf("输出数组内容:\n");
for(i=0; i<cnt2; i++)
{
printf("%d ", *(p+i));
}
printf("\n");
// 别忘了释放指针 p,再指向NULL
free(p);
p=NULL;
return 0;
}
- 静态变量---> 存储到静态存储区 ---> 静态局部变量---> 函数调用结束后不会被释放(作用域:其它函数无法调用)--> 只定义和初始化 1 次
- ---> 静态全局变量---> (作用域:只属于此.c文件,其它文件无法访问)
- 静态内存---> 系统自动分配和回收 --> 分配到栈中 --> 生命周期:函数调用结束,出栈释放
- 动态内存---> 程序猿全手动分配和回收 --> 分配到堆中 --> 生命周期:若忘了free() 回收,易导致内存泄漏
- 栈是一种存储结构,而所谓的“堆” ,是指分配内存的排序方式(即堆排序)
(2)多级指针
多级指针就是——指针的指针的指针...
int ***r ---> 指针变量r 只能存放int ** 型变量的地址 ---> 可写成int **(*r) ---> 基类型变成了int **
# include<stdio.h>
int main(void)
{
int i = 35;
int *p = &i; // 指针变量再取地址(指针变量也有地址)
int **q = &p;
int ***r = &q;
printf("i = %d\n", ***r);
return 0;
}
输出结果为 i = 35
(3)跨函数使用动态内存
需要解决一个问题:“如何在主调函数中,使用被调函数中动态分配的内存?”
# include <stdio.h>
# include <stdlib.h>
void DynamicArray(int **); //声明
int main(void)
{
int *p = NULL;
DynamicArray(&p);
printf("**p=%d\n", *p);
return 0;
}
void DynamicArray(int **q)
{
*q = (int *)malloc(sizeof(*q));
**q = 5;
return;
}
输出结果:*p = 5
- 指针变量p指向的内存单元里没有内容;
- p的初始化是在什么时候?DynamicArray函数调用时,构建动态内存空间,p指向这一空间;
- p是指针变量,指针变量也有自己的地址(系统自动分配的),而p初始化是程序猿的事情;
以下是简单变化:给p指向的内存空间添内容
# include <stdio.h>
# include <stdlib.h>
void DynamicArray(int **); //声明
int main(void)
{
// int *p = NULL;
int i = 99;
int *p = &i;
printf("*****初始状态*****\n");
printf("*p=%d\n", *p);
printf("p的地址值%d\n\n",p);
printf("*****形参分配内存*****\n");
DynamicArray(&p); //传参要传int **型的
printf("*****终态*****\n");
printf("*p=%d\n", *p);
printf("p的地址值%d\n",p);
return 0;
}
void DynamicArray(int **q) //指针变量q,基类型int *
{
*q = (int *)malloc(sizeof(*q));
**q = 5; // 给*q指向的内存单元添实际值5
printf("q的地址值:%d\n",q);
printf("*q的地址值:%d\n",*q);
printf("该内存单元实际值为:%d\n\n",**q);
return;
}
指针变量p与二级指针*q指向的是同一内存单元
稍作改动:
把实参改成p试试
# include <stdio.h>
# include <stdlib.h>
void DynamicArray2(int *); //声明
int main(void)
{
int i = 58;
int *p = &i;
printf("*****初始状态*****\n");
printf("*p=%d\n", *p);
printf("p的地址值%d\n\n",p);
DynamicArray2(p);
printf("*****后状态*****\n");
printf("*p=%d\n", *p);
printf("p的地址值%d\n\n",p);
return 0;
}
void DynamicArray2(int *q)
{
q = (int *)malloc(sizeof(*q));
*q = 566666;
printf("q的地址值:%d\n",q);
printf("*q实际值:%d\n\n",*q);
return;
}
- 发现并没什么卵用,指针变量q未操作p
- 指针变量p指向 i ,q也指向 i ,即q存放了 i 的地址
- 随后调用函数,给q重新分配了空间,q不再指向 i 。Game Over
如何理解?因为指针变量 p 为 int * 型,所以 &p 为 int ** 型,所以形参必须为 int ** 型才可传递
(4)不使用多级指针的跨函数使用动态内存
动态分配的地址,作为函数返回值才有意义
永远不要返回局部变量的地址。
# include <stdio.h>
# include <stdlib.h>
int * DynamicArray3(void);
int main(void)
{
int *p = DynamicArray3();
printf("p=%d\n", p);
printf("*p=%d\n", *p);
return 0;
}
int * DynamicArray3(void)
{
int *q = (int *)malloc(sizeof(*q));
*q = 99;
return q;
}
结果为 *p = 99
(5)指针引用二维数组
假设定义数组int a[3][4];
数组名a,代表的并不是a[0][0]的地址,而是a[0]的地址
有以下推导:
- a == &a[0]
- a[0] == &a[0][0]
- a == &(&a[0][0])
a数组包括3行,a[0]、a[1] 和 a[2] 每个行元素都可看做含有4个元素的一维数组。
- a[0]、a[1] 和 a[2] 既然是一维数组名,那么,该数组名就表示第一个元素的地址。
- a[0] == &a[0][0]
有以下推导:
- a[i] == &a[i][0]
- a[i] +j == &a[i][j]
- *(a+i) == &a[i]
- *(a+i) +j == &a[i][j]
以上记住红色字体即可。
方案一:使用指针变量p,指向二维数组a (贼别扭的定义方式,不推荐)
- 对于a[M][N]而言,二维数组名a是地址的地址
- 数组名 a 的类型为 int(*)[N]
- 所以,先把指针变量 p 定义为 int(*)[N]型才能赋值
- int (*p) [N] = a; // 不能直接将a赋值给p
有以下推导:
- p == a // 指针变量 p 指向首地址a[0]
- a+i == &a[i]
- p+i == &a[i]
- *(p+i) == a[i]
- *(p+i) + j == a[i] + j
- *(p+i) + j == &a[i][j] // 每个元素的地址
- *(*(p+i) + j) == a[i][j] // 取到实际值
方案二:稍作变化,将 &a[0][0] 赋值给指针变量 p
- int *p = &a[0][0];
- p == &a[0][0]
- p + i*N + j == &a[i][j]
- *(p + i*N + j) == a[i][j] // 取到实际值
- 其中N代表列数,对于二维数组a[M][N]
# include <stdio.h>
int main()
{
int a[3][4] = {1,2,3,4,10,20,30,40,22,44,66,88};
int i, j;
int *p = &a[0][0];
for( i=0; i<3; i++)
{
for( j=0; j<4; j++ )
{
printf("%-2d\x20", *(p + i*4 +j));
}
printf("\n");
}
return 0;
}