什么是指针
C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节都有唯一的内存地址。
CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。数据对象是指在内存中的一个指定数据类型的数值或字符串,它们都有自己的地址,而指针就是保存这个地址的变量所以得到:指针变量是一种保存变量地址的变量
指针:内存位置的直接地址
定义:存放变量的地址需要一种特殊类型(指针类型)的变量
为什么要使用指针
- 指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效
- C语言中一些复杂的数据结构往往需要使用指针来构建,如链表,二叉树等
- C语言是传值调用,但是有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且不违背传值调用
指针的声明
声明并初始化一个指针
数据类型(指针变量基类型)* 指针变量名;
例如:
int *p; // 声明一个 int 类型的指针 p
char *p // 声明一个 char 类型的指针 p
int *arr[10] // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
int (*p)() //声明一个指向函数的指针,该函数返回整型
int **p; // 声明一个指针 p ,该指针指向一个 int 类型的指针
指针的声明比普通变量的声明多了一个一元运算符 ' * ' (取值操作符/间接寻址操作符)
注意:
声明一个指针变量并不会自动分配内存,在对指针进行间接访问之前,指针必须进行初始化,或是指向现有的内存,或者给他动态分配内存,否则我们不知道指针指向何处,这是一个很严重的问题
-
指针变量的值(指针变量中存放的值)是地址(即指针)
-
只有与指针变量基类型相同的变量才能存放到指针变量中
-
指针变量中只能存放地址
初始化操作如下:
/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址
/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free(p); // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h
"&" 取址运算符和 "*" 取值运算符
这两个运算符的优先级别相同,按照自右向左方向结合
例如:
int a = 5;
int *p;
//这两个操作相同,都是取 a 的地址
p = &a;
p = &*p; //先执行 *p 的操作,再取址
*&a的含义:
先进行 &a 运算,得到 a 的地址,再 ' * ' 取值运算,即就是变量 a 本身
未初始化和非法指针
如果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,这时,程序会报错。它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改。
NULL指针
NULL指针是一个特殊的变量,表示不指向任何东西。指针的地址为nil,即空值,也可以理解为0。在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是为操作系统保留的。但是,内存地址 0 有一个特别重要的意义,它表明该指针不指向一个可访问的内存位置。
数组与指针
C语言中,数组名代表存放数组元素的连续存储空间的首地址,即指向数组中第一个元素的指针常量
指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素地址放到一个指针变量中)
数组元素的指针就是数组元素的地址
一维数组与指针
定义一个指向数组的指针:
//数组的数据类型应与指针的基类型一致
int a[10];
int *p,*t;
t = a; //指针指向数组首地址,即第一个元素
p = &a[2]; //指针指向数组第三个元素
数组元素的访问
下标法
int a[5],i;
for(i = 0;i < 5;i ++)
{
printf("%d ",a[i]);
}
地址法
int a[5],i;
for(i = 0;i < 5;i ++)
{
printf("%d ",*(a + 1)); //表示取出首地址元素后的第 i 个元素的内容,即下标为 i 的元素 a[i]
}
指针法
int a[5],i;
int *p;
for(p = a;p < (a + 5);p ++)
{
//p ++是向前移动一个元素位置
printf("%d ",*p)
}
注:
p + 1 是访问下一个地址,不存在赋值操作,不改变指针指向;p ++改变指针指向(有赋值操作),相当于是 p = p + sizeof ( 指针变量的基类型 )
虽然 a 和 p 的值都是数组的首地址,但是 a 不能像使用指针变量 p 那样执行自增自减的操作。因为 p 是指针变量,可以通过赋值操作改变它的值,使 p 指向数组中其他元素。数组名 a 是指针常量(代表一个地址常量),其值是不能改变的。
*p ++ , *(p ++) 和 (*p) ++ 的区别
"++" 和 " * " 为同级运算符,结合方向自右向左,所以 *p ++ 先取值,然后地址移一位,*(p ++) , p ++为一个整体,本身含义就是先运算后加一,运算指的是 p++作为一个整体与前面的 * 进行运算,所以 *p ++ = *(p ++)
(*p) ++ 先取值,值加一(地址不动)
只有 ++ (*p) , (*p) ++ , ++*p 指针位置不变,数据 ++,其他都是指针指向下一地址。
函数中改变数组元素的值(实参与形参的对应关系)
形参,实参都为数组
void f(int x[],int n)
{
......
}
void main()
{
int a[10];
f(a,10);
}
实参为数组名,形参用指针变量
void f(int *a,int n)
{
......
}
void main()
{
int a[10];
f(a,10);
}
实参,形参都用指针变量
void f(int *p,int n)
{
......
}
void main()
{
int a[10];
int *p = a;
f(p,10);
}
实参用指针变量,形参为数组名
void f(int x[],int n)
{
......
}
void main()
{
int a[10],*p = a;
f(p,10);
}
指针数组和数组指针
指针数组是一个数组,数组中的每个元素都是指针
声明一个指针数组:
int *p[10];
[ ] 的优先级比 * 高,所以 p 先与 [ ] 结合,成为一个数组 p[ ] ; 再由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针,即 p[ i ] 是一个指针
数组指针是一个指针,它指向一个数组
声明一个数组指针:
int (*p)[10];
由于 ( ) 的优先级最高,所以 p 是一个指针,指向一个 int 类型的一维数组,这一维数组的长度是10,这就是指针 p 的步长。也就是说,执行 p + 1 时, p 要跨过 n 个 int 型数据的长度
二维数组与指针
-
a[ i ] 即 *( a + i ) 可以看成是一维数组的 a 的下标为 i 的元素。同时 a[ i ] 又可以看成是由 a[ i , 0 ] , a[ i , 1 ] , a[ i , 2 ] 这三个元素组成的一维整型数组的数组名,代表 a[ i , 0 ] 的地址,即 &a[ i , 0 ]
-
a[ i , j ] 即 *( *(a + i) + j ) 代表这个数组中下标为 j 的元素地址,即&a[ i , j ]
-
a[ i , j ] ——> *( a[ i ] + j ) ——> *( *(a + i) + j ) ——> *(a + i)[ j ]
如上图,可定义:
int (*p)[3];
// 数组指针
字符串与指针
例如:
char *p = "hello";
或者
char *p;
p = "hello";
将保存在常量储存区中的 " hello " 的首地址赋给指针变量 p (不可修改)
[[字符串#注意:]]
将字符串 a 复制给字符串 b
下标法
//*(a + i) 为 a[i]
for(i = 0;*(a + i) != '\0';i ++)
{
b[i] = a[i]; //或者 *(b + i) = *(a + i);
}
b[i] = '\0'; //或者*(b + i) = '\0'(字符串结束的标志)
指针法
char a[] = "hello";
char b[40],*p1,*p2;
p1 = a;
p2 = b;
for(;*p1 != '\0';p1 ++,p2 ++)
{
*p2 = *p1;
}
*p2 = '\0';
函数调用实现字符串的复制
字符数组作参数
void copy_str(char from[],char to[])
{
int i = 0;
while(from[i] != '\0')
{
to[i] = from[i];
i ++;
}
to[i] = '\0';
}
字符指针变量作形参
//实参传入时,应为字符数组,字符指针指向的字符是常量存储区中的字符串,不可修改
void copy_str(char *from,char *to)
{
while((*to ++ = *from ++) != '\0');
}
字符数组和指针变量的区别
1. 组成成分
字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),不是将字符串放到字符指针变量中
2.赋值
字符数组
- 定义的时候直接用字符串初始化
char a[10] = "hello";
注意:
不能先定义再对数组进行赋值,例如 :
char a[10]; a[10] = "hello";
//这样是错误的
只有定义初始化时才能这样赋值
-
对数组中字符逐个赋值
char a[10] = {'h','e','l','l','o'};
-
利用 strcpy(比较推荐)
char a[10]; strcpy(a,"hello");
易错情况
char a[10]; a[10] = "hello";
,一个字符不能容纳一个字符串,且 a [10] 不存在char a[10]; a = "hello";
,a 虽然是指针,但是它已经指向再堆栈中分配的十个字符空间,现在这个情况 a 又指向数据区的 hello 常量,这里的指针 a 出现混乱,不允许 ! 会出现 const char 无法转换为 char 类型
字符指针变量
char *s = "hello"; == char *s; s = "hello";
函数与指针
函数指针(指向函数的指针)
一个函数在编译时被分配给以个入口地址,函数指针指向函数的入口地址
声明一个函数指针:
返回值类型 (*指针变量名)([形参列表]);
注意!!!
指针变量名要用括号括起来,不可省略,否则:
int *p(void *,void *);
表明 p 是一个函数,返回一个指向 int 类型的指针(指针函数)
例如:从键盘读入两行字符串,判断二者是否相等
#include "stdio.h"
#include "string.h"
int str_comp(const char *m,const char *n); // 声明一个函数 str_comp,该函数有两个 const char 类型的指针,函数的返回值为 int 类型
void comp(char *a,char *b,int (*prr)(const char *,const char*)); // 声明一个函数 comp ,注意该函数的第三个参数,是一个函数指针
int main()
{
char str1[20]; // 声明一个字符数组
char str2[20];
int (*p)(const char *,const char *) = str_comp; // 声明并初始化一个函数指针,该指针所指向的函数有两个 const char 类型的指针,且返回值为 int 类型
gets(str1); // 使用 gets() 函数从 I/O 读取一行字符串
gets(str2);
comp(str1,str2,p); // 函数指针 p 作为参数传给 comp 函数
return 0;
}
int str_comp(const char *m,const char *n)
{
// 库函数 strcmp 用于比较两个字符串,其原型是: int strcmp(const char *s1,const char *s2);
if(strcmp(m,n) == 0)
return 0;
else
return 1;
}
/* 函数 comp 接受一个函数指针作为它的第三个参数 */
void comp(char *a,char *b,int (*prr)(const char *,const char*))
{
if((*prr)(a,b) == 0)
printf("str1 = str2\n");
else
printf("str1 != str2\n");
}
指针函数
指针函数的本质是函数,函数返回值是一个地址值(用于需要地址的情况)
指针函数的声明:
返回值类型 *指针变量名([形参列表]);
指向指针的指针(多级间接寻址)
指向指针的指针的定义
char **p;('*'运算符自右向左)
相当于*(*p)
例如:
char *n[] = {"hello","world","chana"};
char **p;
for(int i = 0;i < 3;i ++)
{
p = n + i; //指向下一个元素
printf("%s",*p);
}
输出结果:
hello world chana