前言
在学习c语言和c++的过程当中,指针是一个重难点,指针这一概念较为抽象且知识点零碎,本章将我在学习c的过程当中遇到的比较基础的指针用法进行汇总,可能会有遗漏,还请读者积极指出、多多包涵。
目录
指针的基础知识
地址
首先,c语言程序中的变量的值都是存储在计算机的内存中指定的位置上的,内存中的每个单元都有唯一的地址(编号),而获取这个地址的运算符就是取地址运算符,即&(shift键+7)。
C++中:
int a = 0;
cout<<&a<<endl;
在vscode中的运行结果为0x61fe1c
c语言中:
int a = 0;
printf("a的地址为%p",&a);
在vscode中的运行结果为000000000061FE1C
指针变量
存放上面展示的&a的变量并非普通的变量可以做到的,若这么做的话
int a = 0;
int p;
p = &a;
在vscode中会给出这样的问题提示:“int * 类型的值不能分配到 int 类型的实体”。这其实说明了&a是一个int *类型的变量,自然就不能赋值给int类型的变量p了。
因此,要存放一个地址就需要一种特殊类型的变量了,即我们今天的主角指针变量。
定义的形式: 类型关键字 *指针变量名
例如:int *p; 这就代表着我们定义了一个名为‘p’的指针变量了。其中类型关键字就是我们指针变量要指向的那个变量的数据类型,例如常见的int,double,char等等。
定义多个指针变量:int *px,*py;
但此时的指针并没有指向一个地址,指针变量的值是一个随机数,我们称之为野指针,因此还需要初始化。
指针变量的初始化:
//先定义再初始化
int *p,a = 0;
p = &a;
//定义指针的同时初始化
int a = 0;
int *p = &a;
间接寻址符
前面我们使用的&a其实是直接寻址,而我们现在可以通过指针变量和间接寻址符来访问指针变量指向的变量的值。
int a = 10;
int *p = &a;
cout<<p<<' , '<<*p<<endl
在vscode中的运行结果为0x61fe14 , 10
可以看到的是,p为变量a的地址,而*p为a的值,因此输出*p和输出a是等价的,修改*pa的值也就是修改a的值,我们可以像使用a一样的方法来使用*p。
int a = 10;
int *p = &a;
a += 5;
*p += 9;
cout<<*p<<endl;
运行结果为24
按值调用与按引用调用
用普通变量作函数参数时,方式为按值调用,即将实参的一份副本传递给函数使用,因此在函数中对于该副本的改变并不会影响实参的值。这一点可以通过下面这一代码看出:
void Foo(int x);
int main()
{
int a = 10;
Foo(a);
printf("%d",a);
}
void Foo(int x)
{
x += 5;
}
运行结果为10
结果为10而并非15,这是因为实参a始终为10。而要使得实参a的值发生改变,就需要指针来作为函数的参数来实现了。指针通过向函数传递某个变量的地址,从而在函数中改变变量的值。这个过程就是C++中的按引用调用,在c语言中称为模拟按引用调用。
指针与函数
指针变量作函数参数
接着对上面程序进行改编,使得他的运行结果为15:
void Foo(int *p);
int main()
{
int a = 10;
Foo(&a);
printf("%d",a);
}
void Foo(int *p)
{
*p += 5;
}
运行结果为15
和前面介绍过的一样,*p可以当作是一个int类型的变量使用,而它指向的是a的地址,因此函数的参数应该为&a,也可以这样理解:int *p = &a。这样,我们就可以将实参的值在函数中进行改变。
典型的一个例子是在函数中交换两个整数的值:
void swap(int *px,int *py);
int main()
{
int x=20,y=10;
swap(&x,&y);
printf("x = %d,y = %d",x,y);
return 0;
}
void swap(int *a,int *b)
{
int t;
t=*a;
*a=*b;
*b=t;
}
运行结果为x = 10,y = 20
函数返回值为指针
函数的返回值为指针代表函数名前面的数据类型也应该为指针类型,返回值应该为地址。
int s;
int *Sum(int x,int y);
int main()
{
int *n = Sum(10,20);
printf("%d",n);
}
int *Sum(int x,int y)
{
s = x + y;
return &s;
}
需要特别注意的是:
1.*n、*Sum;
2.return &s;
3.s应当定义为全局变量,因为局部变量存放于栈区,当函数调用结束后栈区的变量会被释放,而如果一定要在函数内定义的话,要写成static int s = x + y;
函数指针
函数指针的本质是一个指针,只是指针的地址指向了函数,可以通过指针来调用函数,但不同于数据指针,它不可以进行指针 + 整数的操作。
需要知道的是,函数名就是函数的地址,但有一个细微的差别:
Foo(foo)是将foo函数的地址传递给Foo;
而Foo(foo( ))是将foo函数return之后的值传递给Foo;
定义函数指针: int (*p)(int a,int b);
初始化: p = 函数名;
调用: int ret = p(10,20); 也可以写成int ret = (*p)(10,20);
下面是一个例子:
int Sum(int x,int y);
int main()
{
int (*p)(int x,int y);
p = Sum;
int ret = p(10,20);
printf("%d",ret);
}
int Sum(int x,int y)
{
return x + y;
}
运行结果为30
函数指针除了这样简单的用法,还可以将函数指针作为一个函数的参数。
来看第一个例子,对于同一个长度的式子,Li和Zhang有自己的估计时间的算法:
double Li(int x);
double Zhang(int x);
void estimate(int lines,double (*pf)(int x));
int main()
{
cout<<"How many lines?"<<endl;
int lines;
cin>>lines;
cout<<"Li's estimate is ";
estimate(lines,Li);
cout<<"Zhang's estimate is ";
estimate(lines,Zhang);
return 0;
}
double Li(int x)
{
return x*x*0.1;
}
double Zhang(int x)
{
return x*x*x*0.01;
}
void estimate(int lines,double (*pf)(int x))
{
cout<<(*pf)(lines)<<" seconds"<<endl;
}
第二个例子,通过函数指针compare实现对数组a的交换法排序
void paixu(int a[],int n,int (*compare)(int x,int y));
int shengxu(int x,int y);
int jiangxu(int x,int y);
int main()
{
int a[10] = {5,23,42,74,13,42,39,3,31,64};
paixu(a,10,shengxu);
for(int i = 0;i <= 10;i++)
{
cout<<a[i]<<' ';
}
return 0;
}
void paixu(int a[],int n,int (*compare)(int x,int y))
{
int i,j,k;
for(i = 0;i < n;i++)
{
for(int j = 0;j < n-i;j++)
{
if((*compare)(a[j],a[j+1]))
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
int shengxu(int x,int y)
{
return x > y;
}
int jiangxu(int x,int y)
{
return x < y;
}
字符指针
字符指针是指向字符类型数据的指针变量,因为每个字符串在内存中占据的是连续的地址,并有唯一确定的首地址,因此只要将字符串的首地址赋值给字符指针,就可以让字符指针指向一个字符串。而字符串的字面量就是存储区的首地址。
两种初始化的方式:
char *ptr = "Hello"; or char *ptr; ptr = "Hello";
此时,可以修改的是ptr的指向,但不能对现在指向的东西进行改变。
即 *ptr = 'M' 此时是非法的。但如果字符串"Hello"保存在一个数组中,再用一个字符指针指向它就可以进行此操作。
char str[10] = "Hello";
char *ptr = str; 第二条语句相当于 char *ptr; ptr = str;
而“ptr = str”又等价于ptr = &str[0]
因为数组名str是一个地址,所以str不可以被修改,但可以通过ptr修改,即
*ptr = 'M'; //等价于ptr[0] = 'M';相当于str[0] = 'M';
指针与数组
指针的增1和减1的运行速度很快,因此用指针变量来寻址数组元素可以提高程序的执行效率
初始化:
int num[10],*p;
//方法一:
p = num;
//方法二:
p = &num[0]
定义的时候初始化
int *p = num;
指针操作数组的方法
1.*p = 10;代表着赋值num[0] = 10;
2.*(p + i) 等价于 num[i];
3.p++;
补充:将一行内容读入字符数组的方法:
fgets(arry_name,sizeof(arry_name),stdin);
将字符串赋值给字符指针:
char *str;
strcpy(str, "China");
在某些情况下字符数组与字符指针的作用是类似的:
字符指针和字符数组都可以存放字符串,因此向函数传递字符串时既可以使用字符数组也可以使用字符指针作为函数参数。
函数
向函数传递字符串
【例】要实现字符串的复制可以有以下两种方式
方式一:字符数组
#include <iostream>
using namespace std;
#define N 80
void MyStrcpy(char dststr[],char srcstr[]);
int main()
{
char a[N],b[N];
cout<<"Input a string "<<endl;
fgets(a,sizeof(a),stdin);
MyStrcpy(b,a);
cout<<"The copy is : "<<b;
return 0;
}
void MyStrcpy(char dststr[],char srcstr[])
{
int i = 0;
while (srcstr[i] != '\0')
{
dststr[i] = srcstr[i];
i++;
}
dststr[i] = '\0';
}
方式二:字符指针
#include <iostream>
using namespace std;
#define N 80
void MyStrcpy(char *dststr,char *srcstr);
int main()
{
char a[N],b[N];
cout<<"Input a string "<<endl;
fgets(a,sizeof(a),stdin);
MyStrcpy(b,a);
cout<<"The copy is : "<<b;
return 0;
}
void MyStrcpy(char *dststr,char *srcstr)
{
int i = 0;
while (*srcstr != '\0')
{
*dststr = *srcstr;
dststr++;
srcstr++;
}
*dststr = '\0';
}
从函数返回字符指针
要从函数返回应字符指针,只需要将函数原型定义为字符指针类型,返回值返回一个地址即可:
#include <iostream>
using namespace std;
#define N 80
char *MyStract(char *dststr,char *srcstr);
int main()
{
char first[N*2];
char second[N];
cout<<"Input the first string "<<endl;
fgets(first,sizeof(first),stdin);
cout<<"Input the first string "<<endl;
fgets(second,sizeof(second),stdin);
cout<<"The whole is : "<<MyStract(first,second);
return 0;
}
char *MyStract(char *dststr,char *srcstr)
{
char *pstr = dststr; //保留dststr的首地址
//把指针位置移动到dststr末尾
while (*dststr != '\0')
{
dststr++;
}
while (*srcstr != '\0')
{
*dststr = *srcstr;
dststr++;
srcstr++;
}
*dststr = '\0';
return pstr;
}
指针与二维数组
二维数组的操作与一维数组有一些差异,初学者可能会对其行指针和列指针的用法比较模糊
先定义一个二维数组a
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
首先,二维数组a可看成是由a[0],a[1],a[2]三个元素组成的一维数组
此时,a作为它们的数组名,代表的是第一个元素a[0]的地址
因此,下面两句代码是等价的
printf("a = %p\n",a); //a是数组名,代表第一个元素a[0]的地址
printf("&a[0] = %p\n",&a[0]);
此时,要访问后面a[1]的值也有两种方式
(需要注意,a[i]依然是一个数组元素,但它存储的还是地址)
//a + 1表示首地址所指元素后面的第一个元素的地址,即a[1]的地址
printf("a+1 = %p\n",a + 1);
//*(a+1)即为元素a[1],但需要注意这里的元素仍是个地址,而非具体的数据值
printf("*(a+1) = %p\n",*(a+1));
printf("a[1] = %p\n",a[1]);
此时又可将这三个元素a[0],a[1],a[2]分别看作是由4个元素组成的一维数组,a[i]为他们的数组名,因此a[i]也是这个有4个元素的数组的首地址,故a[0] + 1所表示的是a[0][1]的地址,所以*(a[0] + 1) 就是元素a[0][1]的数据值
因此,下面两句代码是等价的
printf("a[0]+1 = %p\n",a[0]+1);
printf("&a[0][1] = %p\n",&a[0][1]);
而我们要访问只需要通过 * 符号即可
printf("*(a[0] + 1) = %d\n",*(a[0] + 1));
运行结果为 2
所以,a[i]即*(a+i)首先可以被看做是数组a的下标为 i 的元素,又可以被看做是由a[i][0],a[i][1],a[i][2],a[i][3]四的元素组成的一维数组的数组名。a[i]是一个数组名,是这个数组的首地址,是地址。所以a[i] + j就表示地址的加减,它以元素的个数来计算,因此a[i] + j表示的是a[i][j]元素的地址
以下是4种等价表示a[2][1]的方法:
printf("a[2][1] = %d\n",a[2][1]);
printf("*(a[2] + 1) = %d\n",*(a[2] + 1));
printf("*(*(a + 2) + 1) = %d\n",*(*(a + 2) + 1));
printf("(*(a + 2))[1] = %d\n\n",(*(a + 2))[1]);
我们可以联想到之前学过的访问数组的四种方式(数组名两种,指针两种)
int b[3] = {100,200,300};
int *pb = b; //用pb来存贮b的地址
printf("b[1] = %d\n",b[1]);
printf("*(b+1) = %d\n",*(b+1));
printf("pb[1] = %d\n",pb[1]);
printf("*(pb+1) = %d\n\n",*(pb+1));
现在我们先不考虑指针的问题,就先看通过数组名来访问,对于一维数组,我们可以通过下标来访问,也可以通过解引用来访问,对于二维数组,我们就可以对它的每一维进行选择,根据排列组合的知识易知,我们就有上面的4中表示a[2][1]的方式了,这里有一张我自己做的图片:
现在再来讨论二维数组的指针
与一维数组有些不同的是,二维数组这里有两种指针,第一种是行指针,另外一种是列指针
行指针
行指针在定义的时候需要给出列的数目
int (*pl)[4];
此时有两种初始化的方式,和我们的一维数组类似
pl = a; //第一种
pl = &a[0]; //第二种
此时我们又可以用之前说的两两组合的四种方式,通过行指针来访问数组了
printf("pl[2][3] = %d\n",pl[2][3]);
printf("*(pl[2] + 3) = %d\n",*(pl[2] + 3));
printf("(*(pl + 2))[3] = %d\n",(*(pl + 2))[3]);
printf("*(*(pl + 2) + 3)= %d\n\n",*(*(pl + 2) + 3));
列指针
列指针就是将二维数组看成了一个个数为i * j的一维数组,所以与行指针最大的不同点在于列指针不用把每一行的列数写出来
int *pr;
它有三种初始化的方式
pr = a[0]; //第一种
pr = *a; //第二种
pr = &a[0][0]; //第三种
此时pr指向了第0行第0列的地址,即a[0][0]的地址,此时对指针pr加减是以一个整数类型字节为单位进行的,所以我们我们可以通过下面两种方式来得到a[2][3]的值
*(pr + i*n + j)
pr[i * n + j]
我们在用列指针时就不可以像之前那样用两个下标之类的来访问了,只能强行用这两张方式
向函数传递二维数组
二维数组来作为函数的参数类似于一维数组,既可以用指针,也可以用数组
二维数组作函数参数
必须指出第二维的长度,其必须与数组a的二维长度相等
const int N = 4;
void Output(int p[][N],int m,int n);
void Input(int p[][N],int m,int n);
//二维数组作为函数参数时,必须指出第二维的长度,其必须与数组a的二维长度相等
int main()
{
int a[3][4];
Input(a,3,4);
Output(a,3,4);
}
void Input(int p[][N],int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
scanf("%d",&p[i][j]);
}
}
}
void Output(int p[][N],int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
printf("%4d",p[i][j]);
}
printf("\n");
}
}
行指针作函数参数
用行指针时,我们不需要对上面函数的main函数进行修改,只需要对两个函数进行修改即可,即使我们修改了函数原型,但函数里面的东西压根不动的话,其实也没什么影响,因为这两种方法总体上可以等价,根据上面行指针那里的介绍,我们在这里其实总共可以有四种写法,在这里就不都展示了,就放一下和上面代码互补的:
(即我们可以在任何一个维度上用下标或者解引用来操作)
void Input(int (*p)[N],int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
scanf("%d",*(p + i) + j);
}
}
}
void Output(int (*p)[N],int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
printf("%4d",*(*(p + i) + j));
}
printf("\n");
}
}
列指针作函数参数
列指针作为函数的参数就有些和上面的不同了,但本质还是将二维数组看做是一维数组
const int N = 4;
void Input(int *p,int m,int n);
void Output(int *p,int m,int n);
int main()
{
int a[3][4];
Input(*a,3,4);
Output(*a,3,4);
}
void Input(int *p,int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
scanf("%d",&p[i*n + j]);
}
}
}
void Output(int *p,int m,int n)
{
for(int i = 0;i < m;i++)
{
for(int j = 0;j < n;j++)
{
printf("%4d",p[i*n + j]);
}
printf("\n");
}
}
指针数组
数组的类型为指针类型的数组成为指针数组,而我们这里主要讲的是char *类型的(即字符指针)
前面的字符指针那里我们由学到,一个字符指针可以用来存储一个字符串,因此字符指针数组便可以用来存储许多的字符串,正如前面有提到的,我们的字符指针直接表示一个字符串的话不能对其进行修改,所以一般都是让字符指针指向一个字符数组。
我们可以这样定义
char *pstr[N]
具体的用法可以看下面的这个代码示例,这个代码是把输入的多个字符串按长度降序排序之后,将长度最长的那个字符串进行输出
#include <string.h>
#include <stdio.h>
#define N 40
#define Length 100
void Sort(char *ptr[],int n);
int main()
{
int n;
char str[N][Length];
char *pstr[N];
printf("输入字符串的个数\n");
scanf("%d",&n);
getchar();
printf("请输入字符串:\n");
for(int i=0;i<n;i++)
{
pstr[i] = str[i];
gets(pstr[i]);
}
Sort(pstr,n);
printf("最长的字符串是%s",pstr[0]);
return 0;
}
void Sort(char *ptr[],int n)
{
char *temp = NULL;
for(int i=0;i<n-1;i++)
{
for(int j=i+1;j<n;j++)
{
if(strlen(ptr[j])>strlen(ptr[i]))
{
temp = ptr[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
}
动态内存分配
我们之前在定义数组的时候发现其实是可以通过变量来定义数组的长度的,但这是在c99之后才允许的,在c89中并不允许我们这样操作,而且用变量来定义数组长度时会遇到一个问题:不可在定义的同时对数组进行初始化,需要在之后再进行初始化。因此我们一般用宏常量或者全局变量来作为数组的长度,但这么做的话势必会造成存储空间的浪费,因此我们就要用到动态内存分配了,动态分配在c和c++中还有所不同,所以我们分开来讨论。
c语言中的动态内存
c语言的动态内存分配函数在头文件<stdlib.h>当中
malloc()
我们可以通过函数malloc()来进行动态内存的分配,若内存分配成功,就返回一个指向这段内存首地址的指针,但如果没有足够的内存单元,那么就返回空指针NULL
申请一个变量的用法:
int *p = NULL;
p = (int *)malloc(sizeof(int))
此时,我们的指针p就指向了一个整型变量,这个变量没有变量名,只能够通过指针p来进行访问。
申请一个长度为n的数组的用法:
int *p = NULL;
p = (int *)malloc(n * sizeof(int))
free()
在申请的动态内存使用结束之后,我们一定要记得将这个申请的内存进行释放
具体的用法:
free(p)
c++中的动态内存
new
c++中分配动态内存靠new函数
申请一个变量的用法:
int *p = new int;
此时,我们的指针p就指向了一个整型变量,这个变量没有变量名,只能够通过指针p来进行访问。
申请一个动态数组的用法:
int *p = new int [20];
此时我们可通过指针p的加减或者指针下标的方式来访问数组的元素。
delete
c++中释放动态内存靠delete函数
删除一个指针变量指向的变量:
delete(p);
删除一个指针变量指向的数组:
delete [] p;
总结
本篇文章到这里也差不多要结束了,可以看到的是:指针是一个非常强大的工具,本篇文章只是举例了一些比较基础的用法,指针在c语言和c++中还大有用途,例如链表,树,图,哈希表,排序算法,查找算法等等,这些内容会在以后的文章中涉及,目前本人也只是一名普通的大一一学生,如果有什么总结上的错误和不足还请多多包涵和指正。