回看经典!第八章 C语言指针完全指南:深度解析二维数组、多级指针与内存操作实战(2015年C语言培训班笔记重读)
本文系统剖析C语言指针的核心机制与高级应用。深度讲解指针与二维数组的交互,厘清数组指针与指针数组的关键区别。涵盖多级指针、函数指针、指针作为函数参数及返回值的完整用法,并详解memset、memcpy等内存操作函数。最后,通过命令行参数处理等实战案例,展示指针的综合应用,助你从根本上掌握指针这一C语言灵魂。
目录
本文深入C语言灵魂——指针,系统剖析其与二维数组的复杂交互(行列指针、数组指针/指针数组辨析),详解多级指针、函数指针及指针作为函数参数的完整应用。最后,通过内存操作函数(memset/memcpy) 与命令行参数处理实战,带你从原理到应用彻底掌握指针。
第八章 指针与二维数组
第1课:复习--指针定义与使用
int a=10;
int *p=&a; 等同于:
int *p;
p=&a;
在C语言中,NULL定义为0
int *p; //P为野指针,避免使用野指针,更不能读写野指针指向的内存
int *p1;
p1=NULL //p1为空指针
野指针不是空指针
float在32位系统下是4个字节
float a=3.14;
char *p=&a;
printf("%x,%x,%x,%x\n",*p,*(p+1),*(p+2),*(p+3));
return 0;
}
结果为:

巧妙修改 指向常量的指针处的值
int main(){
int a=100;
const int *p=&a;
//*p=200; //不能将const int *作为左值使用(不能修改指向的值),会报错
int *p1=p; //把指向常量的指针赋值给别的指针后,其指向的值就可以被修改了,这个是C语言不够严谨的地方
*p1=200;
printf("a=%d\n",a);
return 0;
}
结果为:a=200
巧妙修改常量的值
int main(){
const int ABC=100;
//ABC=300; //不能修改常量的值,会报错
int *p=&ABC;
*p=300; //可以用指针来修改常量的值,这也是C语言不严谨之处。
printf("%d\n",ABC);
return 0;
}
指针常量
指针常量的意思是一旦定义之后,就固定指向一个地址,不能再改变为其它地址。
数组名也是常量
int main(){
int buf[10]={1,2,3,4,5,6,7,8,9,10};
int *p=buf+5;
//p++;
//buf++; //buf=buf+1 //报错,buf是数组名,是一个常量,常量不能做左值。
printf("%d\n",*p);
return 0;
}
结果为:6
思考一个问题:
int buf[10]={1,2,3,4,5,6,7,8,9,10};
int *p=buf;
printf("%d,%d\n",sizeof(p),sizeof(buf));
结果输出什么?
答:结果为4,40
指针也可以使用数组下标
int main(){
int buf[10]={1,2,3,4,5,6,7,8,9,10};
int *p=buf;
int i;
for(i=0;i<10;i++){
printf("%d,",p[i]); //C语言允许指针通过数组下标的方法访问数组成员。p[i] 等同于 *(p+i)
}
return 0;
}
结果为:

用指针修改数组成员值的几种方法
int main(){
int buf[10]={0,1,2,3,4,5,6,7,8,9};
int *p=buf;
int i;
//方法1
*(p+2)=20;
//方法2
p[6]=60;
//方法3
p+=8;
*p=80;
p=buf; //把p的值恢复
for(i=0;i<10;i++){
printf("%d,",p[i]);
}
return 0;
}
结果为:

方法1和方法2都没有修改p的值,但方法三修改了p的值
思考一个问题:
在上面的代码中,执行p+=8; 之后,再执行 p[1]=90; 那么它将修改数组中的哪个成员值?
答:将会修改buf[9]的值。
解释:p[1]等同于*(p+1),执行p+=8;后,p指向的是buf[8],因此…………
第2课:指针数组与多级指针
定义一个指针数组
int main(){
int *a[10]; //定义一个指针数组,共10个成员,每个成员都是int *类型
short *b[10];
char *c[10];
printf("%d,%d\n",sizeof(a),sizeof(a[0]));
printf("%d,%d\n",sizeof(b),sizeof(b[0]));
printf("%d,%d\n",sizeof(c),sizeof(c[0]));
return 0;
}
结果为:(结果都一样)

指向指针的指针(二级指针)
指针就是一个变量,既然是变量,也就存在内存地址,所以可以定义一个指向指针的指针。
定义一个二级指针(两个*代表二级指针)
int main(){
int a=10;
int *p=&a;
int **pp=&p; //定义了一个二级指针,指向了一个一级指针的地址
**pp=100; //通过二级指针,修改内存的地址。
//*pp=10 //相当于将p指向了编号为10的内存。极可能报错
printf("%d\n",a);
return 0;
}
结果为:100
二级指针与一级指针的关系图:
int a=10;
int *p=&a;
int **pp=&p;

三级指针:
通过***ppp访问变量a的值
通过**ppp访问指针p的值
通过*ppp访问pp的值
二级指针:
通过**pp访问变量a的值
通过*pp访问指针p的值
一级指针:
通过*p访问变量a的值
上面所谓的值,是指内存单元中保存的数据
定义一个三级指针
int a=10;
int *p=&a;
int **pp=&p;
int ***ppp=&pp;
***ppp=100;
printf("a=%d\n",a);
结果为:a=100
案例:
int main(){
int a=10;
int *p=&a;
int **pp=&p;
int ***ppp=&pp;
a=ppp; //等同于a=&pp;
printf("%x,%x,%x,%x,%x\n",***ppp,&p,&pp,&ppp,*ppp);
return 0;
}
结果为:

以此类推,可以定义3级甚至更多级指针,C语言允许定义多级指针,但是指针级数过多会增加代码的复杂性,实际编程时最多用到3级,且3级指针也不常用。
一级和二级指针是大量使用的。
第3课 多级指针及变量间的关系
案例:三级指针、二级指针、一级指针、所指向变量间的关系
int main(){
int a=20;
int *p=&a;
int **pp=&p;
int ***ppp=&pp;
printf("a=%x (16进制)\n",a);
printf("&a=%x\n",&a);
printf("------------\n");
printf("p=%x\n",p);
printf("*p=%x\n",*p);
printf("&p=%x\n",&p);
printf("------------\n");
printf("pp=%x\n",pp);
printf("*pp=%x\n",*pp);
printf("**pp=%x\n",**pp);
printf("&pp=%x\n",&pp);
printf("------------\n");
printf("ppp=%x\n",ppp);
printf("*ppp=%x\n",*ppp);
printf("**ppp=%x\n",**ppp);
printf("***ppp=%x\n",***ppp);
printf("&ppp=%x\n",&ppp);
return 0;
}
结果为:


第4课:数组指针--指向多维数组的指针
指向二维数组的指针

案例:
#include <stdio.h>
int main(){
int buf[2][3]={{1,2,3},{4,5,6}};
//int *p[10] //定义指针数组
int (*p)[3]; //定义一个指针,指向二维数组的指针,指向int [3]类型的数据,
p=buf;
//p++ //指向第二行
int i=0;
int j=0;
printf("%d\n",sizeof(p));
printf("%d,%d,%d\n",p,p+1,p+2);
for(i=0;i<2;i++){
for(j=0;j<3;j++){
printf("%d\n",p[i][j]);
}
}
return 0;
}
结果为:

注:p+1位移大小为:1个sizeof(int [3])
p+2位移大小为:2个sizeof(int [3])
sizeof(int [3]也就是二维数组中一行的长度。一行3个int类型的元素,共12个字节。因此:
p+1位移大小为:12个字节
p+2位移大小为:24个字节
p[i][j] 等同于 *(*(p+i)+j)
第5课 以指针变量传参
指针变量做为函数的参数
函数的参数可以是指针类型,它的作用是将一个变量的地址传送给另一个函数。
通过函数的指针参数可以间接的实现形参修改实参的值。
案例如下:
void test(int *n){
(*n)++;
}
int main(){
int i=100;
test(&i);
printf("i=%d\n",i);
return 0;
}
结果为i=101
一维数组名作为函数参数
当数组名作为函数参数时,C语言将数组名解释为指针。
案例如下:
#include <stdio.h>
void test(int buf[]){ / / buf[] 等同于 *buf,数组名代表数组首地址
buf[0]=100;
buf[1]=200;
printf("buf=%d\n",sizeof(buf));
}
int main(){
int buf[5]={10,20,30,40,50};
int i=0;
test(buf); // 这里 buf 与 &buf、&buf[0] 等同
printf("buf=%d\n",sizeof(buf));
for(i=0;i<5;i++){
printf("%d,",buf[i]);
}
return 0;
}
结果为:

案例:智能打印数组成员(无需写具体成员个数)
void print_buf(int *buf,int n){
int i;
for(i=0;i<n;i++){
printf("%d,",buf[i]);
}
}
int main(){
int buf[]={10,20,30,40,50,60,92};
print_buf(buf,sizeof(buf)/sizeof(int));
return 0;
}
二维数组名作为函数参数
二维数组作为函数参数时可以不指定第一个下标。
将二维数组作为函数的参数的情况不是特别常用。
案例:
void print_buf(int (*p)[3],int a,int b){
int i,j;
for(i=0;i<a;i++){
for(j=0;j<b;j++){
printf("p[%d][%d]=%d\n",i,j,p[i][j]);
}
}
}
int main(){
int buf[2][3]={{1,2,3},{4,5,6}};
print_buf(buf,sizeof(buf)/sizeof(buf[0]),sizeof(buf[0])/sizeof(int));
return 0;
}
结果为:

void print_buf(int (*p)[3],int a,int b) 等同于:
void print_buf(int p[][3],int a,int b)
const关键字保护数组内容
如果将一个数组做为函数的形参传递,那么数组内容可以被调用函数修改,有时不希望这样的事情发生,所以要对形参采用const参数。
案例如下:
void mystrcat(char *s1,const char *s2){ //第一个参数要被修改,第二个参数不被修改
int len=0;
int i;
while(s2[len]){
len++;
}
while(*s1){
s1++;
}
for(i=0;i<len;i++){
*s1=*s2;
s1++;
s2++;
}
}
int main(){
char s1[10]="abc";
char s2[10]="efg";
mystrcat(s1,s2);
printf("s1=%s\n",s1);
return 0;
}
结果为:

指针作为函数的返回值
案例如下:
char *mystr(char *s,char c){
while(*s){
if(*s==c)
return s;
s++;
}
return NULL;
}
int main(){
char str[100]="hello world";
char *s=mystr(str,'o');
printf("%s\n",s);
return 0;
}
结果为:

为什么结果是:o world ?
因为mystr(str,'o')返回的是一个指向字符 'o'的地址,而printf("%s\n",s);是把这个地址中的内容以字符串的形式打印出来。因此它会把 'o'和后面的字符全打印出来,直到碰到结尾符0
第6课:指向二维数组的指针计算二维数组行列的平均值
方法一、先用数组下标方式实现:
代码如下:
#include <stdio.h>
int main(){
int buf[3][5]={{2,5,8,9,4},{3,6,7,2,9},{5,8,7,2,1}};
int i,j;
for(i=0;i<3;i++){
int sum=0;
for(j=0;j<5;j++){
sum+=buf[i][j];
}
printf("buf[%d][?]/5=%d/5=%d\n",i,sum,sum/5);
}
printf("------------\n");
for(i=0;i<5;i++){
int sum=0;
for(j=0;j<3;j++){
sum+=buf[j][i];
}
printf("buf[?][%d]/5=%d/5=%d\n",i,sum,sum/5);
}
return 0;
}
结果如下:

方法二、用指向二维数组的指针实现,只需要用*(*(a+1)+2)替换buf[i][j]即可
代码如下:
#include <stdio.h>
int main(){
int buf[3][5]={{2,5,8,9,4},{3,6,7,2,9},{5,8,7,2,1}};
int i,j;
for(i=0;i<3;i++){
int sum=0;
for(j=0;j<5;j++){
sum+=*(*(buf+i)+j);
//sum+=buf[i][j];
}
printf("buf[%d][?]/5=%d/5=%d\n",i,sum,sum/5);
}
printf("------------\n");
for(i=0;i<5;i++){
int sum=0;
for(j=0;j<3;j++){
sum+=*(*(buf+j)+i);
//sum+=buf[j][i];
}
printf("buf[?][%d]/5=%d/5=%d\n",i,sum,sum/5);
}
return 0;
}
第7课:函数指针与memset函数
指针可以指向变量,数组,也可以指向一个函数。
一个函数在编译时会分配一个入口地址,这个入口地址就是函数指针,函数名就代表函数的入口地址。
函数定义指针变量的形式为:
函数返回类型(*指针变量名称)(参数列表)
函数可以通过指针调用
指向函数的指针
int add(int a,int b){
return a+b;
}
int main(){
int i=0;
int (*p)(int,int); //定义了一个指向函数的指针,可以
p=add; //直接写函数的名字,代表函数的地址,将add这个函数的地址赋值给指针变量p
i = p(5,8); //通过指针变量间接调用指针指向的函数
printf("%d\n",i);
return 0;
}
结果为:13
int (*p)(int,int); 这样定义指向函数的指针时,此指针能指向的函数必须具备以下条件:有两个int类型参数,有int类型的返回值。
有这么一个函数:
void function(int *s,int n){
....................
}
请问如何,定义一个指向它的指针?
可以这样:void (*p)(int *s,int n);
也可以这样:void (*p)(int *,int);
void *p(int,char *); 表示什么意思?
答:声明了一个函数,函数名叫p,返回值类型为void *,函数的参数类型是int和char *
void (*p)(int,char*) //定义一个指向参数为int和char*,返回值为void的函数的指针。
int *(*p)(int *); //定义一个参数为int *,返回值为int *的指向函数的指针
void (*p)(); //定义一个指向没有参数,没有返回值的函数的指针。
在回调函数和运行期动态绑定的时候大量的用到了指向函数的指针。
案例:
void man(){
printf("吃喝嫖赌\n");
}
void woman(){
printf("坑蒙拐骗\n");
}
int main(){
int i=0;
void (*p)();
scanf("%d",&i);
if(i==1)
p=man;
else if(i==2)
p=woman;
p();
return 0;
}
把指向函数的指针做为函数的参数
将函数指针做为另一个函数的参数称为回调函数。
int fun(int(*p)(int,int),int a,int b){ //第一个参数是指向函数的指针
return p(a,b); //通过指向函数的指针调用一个函数
}
int add(int a,int b){
return a+b;
}
int main(){
int i=0;
i=fun(add,7,8); //add函数在这里就叫回调函数
printf("i=%d\n",i);
return 0;
}
结果为:i=15
memset,memcpy,memmove函数
这三个函数分别实现内存设置,内存拷贝和内存移动
使用memcpy的时候,一定要确保内存没有重叠区域。
它们需要一个头文件: #include <string.h>
案例:将数组初始化
#include <stdio.h>
#include <string.h>
int main(){
int i;
int buf[10]={0}; //只能用于定义数组的时候同时初始化内容,定义完成后,再用 buf[10]={0}是不行的,这是错误语法
buf[0]=10;
buf[6]=60;
buf[8]=20;
//想将这个buf再一次初始化为0,可以用下面方式,但效率不高
//for(i=0;i<10;i++){
// buf[i]=0;
//}
memset(buf,0,sizeof(buf)); //将一块内存初始化为0,最常用的方法
for(i=0;i<10;i++){
printf("buf[%d]=%d\n",i,buf[i]);
}
return 0;
}
memset(buf,0,sizeof(buf)); //第一个参数是要设置的内存地址,第二个参数是要设置的值,第三个参数是内存大小,单位:字节
案例:将数组1拷贝到数组2中:
#include <stdio.h>
#include <string.h>
int main(){
int i;
int buf1[10]={1,2,3,4,5,6,7,8,9,10};
int buf2[10]={0};
memcpy(buf2,buf1,sizeof(buf1));
for(i=0;i<10;i++){
printf("buf2[%d]=%d\n",i,buf2[i]);
}
return 0;
}
注:memcpy(buf2,buf1,sizeof(buf1));将buf1的内存内容全部拷贝到buf2中,拷贝大小为第三个参数的大小,单位为:字节。
案例:将buf1中的数组成员移动到buf2中,代码跟拷贝一样:
#include <stdio.h>
#include <string.h>
int main(){
int i;
int buf1[10]={1,2,3,4,5,6,7,8,9,10};
int buf2[10]={0};
memmove(buf2,buf1,sizeof(buf1));
for(i=0;i<10;i++){
printf("buf2[%d]=%d\n",i,buf2[i]);
}
return 0;
}
使用内存移动函数memmove后,源内存中的数据仍然存在,没有变化。效果跟memcpy是一样的,那么这两个函数有什么区别呢?请看第8课
第8课:指针总结
memmove与memcpy的区别在于:
使用memcpy的时候,一定要确保内存没有重叠区域。案例如下:
#include <stdio.h>
#include <string.h>
int main(){
int i;
int buf[10]={1,2,3,4,5,6,7,8,9,10};
int *start=buf+3;
int *end=buf+5;
memmove(start,end,16); //这里就产生了内存重叠区域
for(i=0;i<10;i++){
printf("buf[%d]=%d\n",i,buf[i]);
}
return 0;
}

指针总结:

int main(){
int *p[3]; //定义一个指针数组,有3个int *成员,
int a,b,c;
p[0]=&a
p[1]=&a;
p[2]=&a;
*p[0]=10; //通过指针数组成员访问指针指向内存的值。
int (*p)[3]; //定义一个数组指针,是一个名字叫p的指针变量,他指向 int [3] 这种数据类型,数组指针大多数情况下用在指向一个二维数组。
int buf1[5][3];
int buf2[7][3], //只要二维数组的第一维是int [3]这种类型,那么都可以用int (*p)[3] 来指向他
return 0;
}
案例:对数组指针的深入了解
int main(){
int buf[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4]; //p++移动4*sizeof(int)个字节,(*p)++移动sizeof(int)个字节
p=buf;
printf("%p,%p,%p,%p,%p,\n",buf,buf[0],&buf[0][0],p,*p);
p++;
printf("%p,%p,%p,%p,%p,\n",buf,buf[1],&buf[1][0],p,*p);
printf("%p,%p\n",(p+1),(*p+1));
return 0;
}
结果为:

如何理解指向二维数组的指针呢?
可以把二维数组buf[3][4]看作是一行有4个成员,共3行的表格。
int (*p)[4];可以指向一行有4个成员,共N行的表格。
p 表示逐行递进,因此p++ 移动4*4=16个字节。
*p 表示逐个成员递进,因此 (*p)++ 移动4个字节。
p+3 移动3*16=48个字节。
*p+3 移动 3*4=12个字节。
buf[3][4]可以看成是如下表格:

p[0], *(p+0), p, *p 表示0行,0列元素地址
p+1 表示第1行首地址
p[1], *(p+1) 表示第1行,0列元素地址
p[1]+2 , *(p+1)+2 , &p[1][2] 表示第1行,2列元素地址
*(p[1]+2), *(*(p+1)+2), p[1][2] 表示第1行,2列元素的值。
第9课 字符指针与字符串
字符指针与字符串
案例:用指针来打印int数组与 打印字符串数组
void print_array(int *p,int n){ //如果参数是int数组,那么就必须传递第二个参数来标识数组的长度
int i;
for(i=0;i<n;i++){
printf("p[%d]=%d,",i,p[i]);
}
}
void print_str(char *s){ //如果参数是字符串,就不需要第二个参数,因为字符串是以0结尾的,以此可判断是否到了字符尾。
int i=0;
while(s[i]){
printf("%c",s[i++]);
}
}
int main(){
int array[5]={1,2,3,4,5};
char str[100]="hello world";
print_array(array,5);
print_str(str);
return 0;
}
指针数组作为main函数的形参
int main(int argc,char *args[]){
int i;
printf("%d\n",argc);
//argc代表程序执行时有几个参数,程序名称本身就是一个参数,所以argc最小值为1
//第二个参数是一个指针数组,其中每个成员的类型是 char *
printf("args[0]=%s\n",args[0]);
//args是一个指针数组,那么他的成员数量是多少呢?argc这个参数就是告诉main函数args这个数组有多少个成员的。
for(i=0;i<argc;i++){
printf("args[%d]=%s\n",i,args[i]);
}
return 0;
}
运行结果为:

然后在cmd中手动运行,并随意输入一些参数,结果如下:

第10课:通过main函数的参数实现计算器
如何打开通过VS创建的windows控制台程序?
假如程序名称为main-a,源文件名为main.c,则要在VS中再次打开、编辑、调试,就应该打开main-a.sln文件:

通过main函数的参数实现计算器
代码如下:
#include <stdio.h>
int main(int argc,char *args[]){
int a=atoi(args[1]);
int b=atoi(args[3]);
if(argc<4){
printf("参数不足,程序返回");
return 0;
}
switch(args[2][0]){
case'+':
printf("%d+%d=%d\n",a,b,a+b);
break;
case'-':
printf("%d+%d=%d\n",a,b,a-b);
break;
case'*':
printf("%d+%d=%d\n",a,b,a*b);
break;
case'/':
if(b){
printf("%d+%d=%d\n",a,b,a/b);
break;
}else{
printf("除数不能为0");
}
}
return 0;
}
通过CMD运行main-a.exe 并输入参数,效果如下:

计算机科学与技术 & 计算机网络技术:双专业课程体系完全导航指南
本系列目录
1、回看经典!第一章 从“Hello World”到理解编译本质(2015年C语言培训班笔记重读)
2、回看经典!第二章 数据类型与运算符详解(2015年C语言培训班笔记重读)
3、回看经典!第三章 流程控制全解 逻辑/分支/循环/图形实战(2015年C语言培训班笔记重读 )
4、回看经典!第四章 · C语言数组与字符串核心实战 从定义、遍历到排序与算法详解(2015年C语言培训班笔记重读)
5、回看经典!第五章 C语言数组高级应用实战:字符串处理、安全防范与多文件编程(2015年C语言培训班笔记重读)
6、回看经典!第六章 C语言综合能力提升:多文件编程与递归函数核心解析及实战(2015年C语言培训班笔记重读)
7、回看经典!第七章 全面解析C语言指针:从核心概念到高效应用(2015年C语言培训班笔记重读)
8、回看经典!第八章 C语言指针完全指南:深度解析二维数组、多级指针与内存操作实战(2015年C语言培训班笔记重读)
9、回看经典!第九章 C语言内存管理完全指南:四区模型、堆栈操作与malloc/free核心实践(2015年C语言培训班笔记重读)
10、回看经典!第十章 C语言结构体完全指南:内存对齐、动态管理与指针应用实战(2015年C语言培训班笔记重读)
11、回看经典!第十一章 C语言文件操作与数据处理实战:从结构体到文本加密、排序全解析(2015年C语言培训班笔记重读)
12、回看经典!第十二章 C语言二进制文件操作实战:fread/fwrite读写、fseek随机访问与加密排序案例(2015年C语言培训班笔记重读)
更多推荐


所有评论(0)