C语言学习——指针
一、指针的概念
? 变量在内存中是一块一块的,每一块都有一个对应的编号,这个编号就是指针。
在C程序中使用指针可以
- 使程序简洁、紧凑、高效
- 有效的表示复杂的数据结构
- 动态分配内存
- 得到多于一个的函数返回值
1.1、变量与地址
? 在计算机内存中,每一个字节单元,都有一个编号,这个编号就是指针。
每一个编号都对应着一个字节,我们知道int型变量占用4个字节。
来一个简单的程序:
#include<stdio.h>
int main()
{
int a=10;
int *aq;
aq=&a;
printf("%d\n%d\n",*aq,a);
printf("%p\n",aq);
return 0;
}
运行结果:
1.2、指针变量和指针的类型
? 指针变量就是一个变量,它存储的内容是一个指针,例如程序中的 int *aq,aq就是指针变量,它的存储内容是0x7ffe02d0c7ec,0x7ffe02d0c7ec实际上就是变量a的地址。
? 我们在定义一个变量的时候要确定他的类型,例如这里我们定义变量a,int a,在定义指针变量时,也要定义指针的类型,int 变量的指针需要用 int 类型的指针存储,float 变量的指针需要用 float 类型的指针存储。
例如,当用aq储存a的地址的时候,必须保证aq和a的类型是一致的,都是int类型。
二、变量指针与指针变量
变量指针就是变量的存储地址,指针变量就是存储指针的变量
2.1、指针变量的使用
-
取地址运算符&:单目运算符&是用来取操作对象的地址。例:&a为取变量 a 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。 -
指针运算符(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。 ? 在上面程序中, printf("%d\n%d\n",*aq,a);
aq已经是变量a的地址,*aq就是通过a的地址访问a的内容,所以 *aq就是10。
2.2、指针变量的初始化
? 指针变量与其它变量一样,在定义时可以赋值,即初始化。也可以赋值“NULL”或“0”,如果赋值“0”,此时的“0”含义并不是数字“0”,而是 NULL 的字符码值。指针必须初始一个值了才能使用。
int x;
int *px = &x;
int *p1= NULL, *p2 = 0;
2.3、指针的运算
(1)赋值运算
? 指针变量可以互相赋值,也可以赋值某个变量的地址,或者赋值一个具体的地址
#include<stdio.h>
int main()
{
int *px,*py,a=100;
px=&a;
py=px;
px=100;
printf("%p\n%d\n",py,px);
}
(2)指针与整数的加减运算
-
指针变量的自增自减运算。指针加 1 或减 1 运算,表示指针向前或向后移动一个单元(不同类型的指针,单元长度不同)。这个在数组中非常常用。 -
指针变量加上或减去一个整形数。和第一条类似,具体加几就是向前移动几个单元,减几就是向后移动几个单元。
#include<stdio.h>
int main()
{
int a=10;
int *aq;
aq=&a;
printf("%d\n%d\n",*aq,a);
printf("%p\n",aq);
printf("%p\n%p\n",aq+1,aq+2);
return 0;
}
输出结果:
? 从运行结果可以看出,aq的值是0x7ffec8o74eec,aq+1是0x7ffec8074ef0,aq+2是0x7ffec8074ef4,我们通过计算可以看出aq+1在aq的基础上相加了4个字节,aq+2在aq的基础上相加了8个字节,因为int型占4个字节。加1就是一个int型占4个字节,加2就是两个两个int型占8个字节。
(3)关系运算
假设有指针变量 px、py。
- px > py 表示 px 指向的存储地址是否大于 py 指向的地址
- px == py 表示 px 和 py 是否指向同一个存储单元
- px == 0 和 px != 0 表示 px 是否为空指针
#include<stdio.h>
int main()
{
int nums[2] = {1,2};
int *p1 = &nums[0];
int *p2 = &nums[1];
int *p3 = &nums[0];
int *p4;
if(p1 < p2)
{
printf("p2的存储地址大于p1\n");
}
if(p1 == p3)
{
printf("p1和p3指向同一个地址\n");
}
if(p4 == NULL || p4 == 0)
{
printf("p4是一个空指针\n");
}
return 0;
}
三、指针与数组
? 数组可以通过下标来访问,还可以通过指针来访问,在数组中,数组名为数组的首地址,通过指针对整数的加减来访问数组中的元素。
3.1、指向数组的指针
来一个简单的程序:
#include<stdio.h>
int main()
{
int sums[10]={1,2,3,4,5,6,7,8,9,10};
int *p;
p=sums;
printf("%d\n%d\n",*(p+2),*(p+4));
}
? 定义了一个名为sums的数组,定义它分配5个连续的int内存空间,而一个数组的首地址即为数组名sums,或者第一个元素的首地址也是数组的首地址。那么有两种方式让指针变量 p 指向数组 sums:
所以输出的*(p+2)=3, *(p+4)=5
1、*p = 1,此操作为赋值操作,即将指针指向的存储空间赋值为 1。此时 p 指向数组 nums 的第一个元素,则此操作将 nums 第一个元素赋值为 0,即 nums[0] = 1。 2、p + 1,此操作为指针加整数操作,即向前移动一个单元。此时 p + 1 指向 nums[0]的下一个元素,即 nums[1]。通过p + 整数可以移动到想要操作的元素(此整数可以为负数)。 3、如上面,p(p + 0)指向 nums[0]、p + 1 指向 nums[1]、、、类推可得,p+i 指向 nums[i],由此可以准确操作指定位置的元素。 4、在 p + 整数的操作要考虑边界的问题,如一个数组长度为 2,p+3 的意义对于数组操作来说没有意义。
注:数组名不等价于指针变量,指针变量可以进行 p++和&操作,而这些操作对于数组名是非法的。数组名在编译时是确定的,在程序运行期间算一个常量。
3.2、字符指针和字符数组
在 C 语言中本身没有提供字符串数据类型,但是可以通过字符数组和字符指针的方式存储字符串。
(1)字符数组方式
下面的声明和初始化创建了一个 RUNOOB 字符串。由于在数组的末尾存储了空字符 \0,所以字符数组的大小比单词 RUNOOB 的字符数多一个。
char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
依据数组初始化规则,可以把上面的语句写成以下语句:
char site[] = "RUNOOB";
以下是 C/C++ 中定义的字符串的内存表示:
(2)字符指针方式
指针方式操作字符串和数组操作字符串类似,可以把定义的指针看做是字符数组的数组名。
#include<stdio.h>
#include<string.h>
int main()
{
char *site = "RUNOOB";
char site1[] = "RUNOOB";
printf("%s\n", site);
printf("%c\n", site[0]);
printf("字符串长度:%d\n", strlen(site));
printf("指针所占内存大小:%d\n" ,sizeof(site));
printf("字符串所占空间大小: %d\n" ,sizeof(site1));
return 0;
}
运行结果:
RUNOOB
R
字符串长度:6
字符指针所占内存大小:8
字符串所占空间大小:7
? 我在计算字符串所占空间大小的过程中,首先使用的是sizeof(site)),但输出结果是8,实际的计算是’R’, ‘U’, ‘N’, ‘O’, ‘O’, 'B’各占一个字节,‘\0‘占一个字节,共占7个字节。后来发现sizeof(site))是计算字符指针的内存大小,实际上指针代表的是寻地址,指针长度实际上和最大的寻址长度是有关系的,32位机器的最大寻址长度为4Byte,所以实际上sizeof(site)就是指针site的地址,但是8位单片机,16位单片机和64位单片机就不一样了,如果8位的单片机用8位的编译器编译,sizeof(site)是1。我们有时候发现为啥我们用64位的电脑编译,发现sizeof§=4呢,因为我们用的编译器是32位的。就是这样的,所以大家平时用的时候要注意了。一般我们用的是32位机,尤其是传递指针的时候,不是数组的长度,而是指针的长度。
字符指针方式区别于字符数组方式,字符数组不能通过数组名自增操作,但是字符指针是指针,可以自增操作。
程序:将数组fl中的内容复制到令一数组word中
#include<stdio.h>
int main()
{
char fl[]="I LOVE YOU",word[100];
char *ch = word;
int i;
for(i=0;fl[i]!='\0';i++)
{
*(ch+i)=fl[i];
}
*(ch+i)='\0';
printf("ch=%s\n,word=%s\n",ch,word);
}
注:指针变量必须初始化一个有效值才能使用
四、动态内存分配
定义数组时数组大小在程序运行时才知道 , 静态开辟就无法实现。
在C中动态开辟空间需要用到三个函数 :
malloc(), calloc(), realloc() ,这三个函数都是向堆中申请的内存空间.
malloc()函数用法:
void * malloc(size_t size)
p=(int*)malloc(sizeof(int) * n);
calloc()函数用法:
void * calloc(size_t num,size_t size)
p=(int*)calloc(n,sizeof(int));
4.1、malloc函数
1).malloc()函数会向堆中申请一片连续的可用内存空间
2).若申请成功 ,返回指向这片内存空间的指针 ,若失败 ,则会返回NULL, 所以我们在用malloc()函数开辟动态内存之后, 一定要判断函数返回值是否为NULL.
3).返回值的类型为void型, malloc()函数并不知道连续开辟的size个字节是存储什么类型数据的 ,所以需要我们自行决定 ,方法是在malloc()前加强制转 ,转化成我们所需类型 ,如: (int)malloc(sizeof(int)*n).
4).如果size为0, 此行为是未定义的, 会发生未知错误, 取决于编译器
来一个程序:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int n=0,i;
int *p = NULL;
scanf("%d",&n);
p=(int*)malloc(sizeof(int) * n);
if(p != NULL)
{
for(i=0;i<n;i++)
{
scanf("%d",&p[i]);
}
for(i=0;i<n;i++)
{
printf("%3d",p[i]+1);
}
free(p);
p = NULL;
}
}
4.2、realloc函数
? realloc()函数让动态内存管理更加灵活 .在程序运行过程中动态分配内存大小, 如果分配的太大 ,则浪费空间, 如果太小, 可能还是会出现不够用的情况 .为了合理的利用内存,我们使用realloc() 函数对内存进行灵活的调整。
程序:将数据文件放入数组中
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE *in,*out;
char ch,infile[10];
int size=0,i;
char a[100];
double *data = (double*)malloc(sizeof(double));
printf("输入文件名称");
scanf("%s",infile);
if((in=fopen(infile,"r"))==NULL)
{
printf("打开文件失败\n");
exit(0);
}
while(fgets(a,100, in) != NULL)
{
double num = atof(a);
data[size] = num;
size++;
data = (double*)realloc(data, (sizeof(double))*(size+1));
}
fclose(in);
for(i=0; i<size-1; i++)
printf("%lf\n",data[i]);
free(data);
}
五、函数与指针
5.1、指针做函数参数
程序:交换两个变量的内容
#include<stdio.h>
int swap(int *a,int *b)
{
int t;
t=*a;
*a=*b;
*b=t;
}
int main()
{
int a=50,b=20;
swap(&a,&b);
printf("%d %d",a,b);
}
5.2、函数返回值为指针
C 语言不允许返回一个完整的数组作为函数的参数。但是,**可以通过指定不带索引的数组名来返回一个指向数组的指针。**如果您想要从函数返回一个一维数组,必须声明一个返回指针的函数。
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
int *gt(int a[])
{
int i;
srand( (unsigned)time( NULL ) );
for(i=0;i<10;i++)
{
a[i]=rand();
}
return a;
}
int main()
{
int *b,j;
int c[10]={1,2,3,4,5,6,7,8,9,0};
b=gt(c);
for(j=0;j<10;j++)
{
printf("%d\n",b[j]);
}
}
5.3、指向函数的指针
C 语言中,函数不能嵌套定义,也不能将函数作为参数传递。但是函数有个特性,即函数名为该函数的入口地址。我们可以定义一个指针指向该地址,将指针作为参数传递。函数指针在进行“*”操作时,可以理解为执行该函数。函数指针不同与数据指针,不能进行+整数操作。
函数指针变量定义方式:
返回值类型 (*变量名)(形式参数类型列表) = NULL;
指向函数:
变量名 = 函数名;
通过函数指针变量调用函数:
(*变量名)(实在参数列表)
函数指针起别名:
typedef 返回值类型 (*类型名)(形式参数类型);
函数指针作用:函数作为函数的参数。
作用:
1、线程创建函数;
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
2、进程编程:信号注册函数;
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数作为函数的参数示例程序:
#include<stdio.h>
int add(int data1,int data2)
{
int res = data1 + data2;
return res;
}
int main()
{
printf("%p\n",add);
int (*pfun)(int,int) = NULL;
pfun = add;
int res = (*pfun)(1,2);
printf("res = %d\n",res);
return 0;
}
六、多级指针及指针数组
6.1、多级指针
? 指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。
程序示例:
#include<stdio.h>
int main()
{
int x = 666;
int *p = NULL;
int **pp = NULL;
p = &x;
pp = &p;
printf("%p %p\n",p,pp);
}
在初始化二级指针 ppi时,不能直接 pp = &&x,因为&x 获取的是一个具体的数值,而具体数字是没有指针的。
6.2、指针数组
? 指针变量和普通变量一样,也能组成数组,指针数组的具体定义如下:
数据类型:*数组名[指针数组长度];
? 指针数组经常与二级指针结合,因为指针数组中的元素都是指针变量,其对应的地址就是二级指针。
程序示例:整形数组、一级指针和二级指针的关系。
#include<stdio.h>
int main()
{
int nums[] = {1,2,3,4,5,6,7,8,9};
int *p[9] = {0};
int **pp;
for(int i = 0; i < sizeof(nums)/sizeof(nums[0]); i++)
{
p[i] = &nums[i];
}
pp = p;
printf("%d %d %d\n",nums[0],*p[0],**pp);
printf("%d %d %d\n",nums[1],*p[1],**(pp+1));
for(int i = 0; i < sizeof(nums)/sizeof(nums[0]); i++)
{
printf("%d ",**(pp+i));
}
return 0;
}
输出结果:
1 1 1
2 2 2
1 2 3 4 5 6 7 8 9
其中二级指针pp指向了指针数组p的首地址,因此可以通过*(pp+i)来访问指针数组p中的元素,而p中元素对应着数组nums中元素的地址,故可以通过**(pp+i)来访问数组nums中的元素。
7、指针与多维数组
7.1、多维数组的地址
以二维数组为例:
int nums[2][2] = {{1,2},{2,3}};
1、先是第一个维度,将数组当成一种数据类型 x,那么二维数组就可以当成一个元素为 x 的一维数组。 2、如上面的例子,将数组看成数据类型 x,那么 nums 就有两个元素。nums[0]和 nums[1]。 3、我们取 nums[0]分析。将 nums[0]看做一个整体,作为一个名称可以用 x1 替换。则 x1[0]就是 nums[0][0],其值为 1。
数组名为函数首地址,则nums为这个数组的首地址,其存储方式可以表示为;
对于三维数组实际存储形式如下:
实际存储内容的为最内层维度,且为连续的。对于 a 来说,其个跨度为 4 个单元;对 a[0]来说,其跨度为 2 个单元;对 a[0][0]来说,跨度为一个单元。有上面还可以得出:
a == a[0] == a[0][0] == &a[0][0][0];
上面的等式只是数值上相等,性质不同。
7.2、多维数组的指针
在前面讲指针数组时,所有指针数组元素都指向一个数字,那么我们现在可以尝试用指针数组的每个元素指向一个数组:
#include<stdio.h>
int main()
{
int nums[2][2] ={{1,2},{3,4}};
int *p[2] = {nums[0],nums[1]};
printf("nums[0][0]地址为:%p\n",nums);
printf("nums[0][1]地址为:%p\n",*p+1);
printf("nums[1][0]地址为:%p\n",*(p+1));
printf("nums[1][0]地址为:%p\n",*(p+1)+1);
}
存储样例如图:
7.3、字符串数组
(1)二维字符数组
const char arry[3][6] = {"apple","banana","orange"};
? 在栈上分配了18字节内存,字符串数组有3个元素,每个元素是6个字节的字符数组,因为数组名arry是指向数组首元素arry[0]的指针,arry[0]的类型是char [10],所以arry的类型是char (*arry)[10]。
反过来我们可以定义char (*myarry)[10] 类型的指针来指向char myarry[][10]的字符串数组,即char (*myarry)[10] = arry。
(2)字符指针数组
const char* arry[] = {"apple","banana","orange"};
? 在栈上分配了sizeof(char*)×4字节(32位编译器为4字节)的内存空间,存储字符串指针,因为数组名arry是指向数组首元素arry[0]的指针,arry[0]的类型是char*,所以arry的类型就是char** arry(等价于char* (*arry)),
7.4、指针的类型
? 从语法的角度看,只要把指针声明语句里的指针名称去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
(1)int* ptr; 指针的类型是 int* (2)char* ptr; 指针的类型是 char* (3)int** ptr; 指针的类型是 int** (4)int(*ptr)[3]; 指针的类型是 int()[3] (5)int*(*ptr)[4]; 指针的类型是 int(*)[4]
7.5、指针所指向的类型
? 当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。从语法上看,只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int*ptr; 指针所指向的类型是 int (2)char*ptr; 指针所指向的的类型是 char (3)int**ptr; 指针所指向的的类型是 int* (4)int(*ptr)[3]; 指针所指向的的类型是int()[3] (5)int*(*ptr)[4]; 指针所指向的的类型是 int*()[4]
? 在指针的算术运算中,指针所指向的类型有很大的作用。指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。
|