- 通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的
- 如果主调函数不使用
return
返回的值,则必须通过地址才能修改主调函数中的值 - 指针变量与普通变量的区别
- 直接访问 -
int a;printf("%d", a);
- 间接访问 - 指针
- 直接访问 -
1. 什么是指针?
- 指针也就是内存地址,指针变量是专门用来存放内存地址的变量
- 指针变量声明的一般形式为:
type *var_name;
- 指针最常见错误:定义了指针变量,还没有指向任何变量,就开始使用指针
- 所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。
- 不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
2. 解引用
2.1. 取地址符&
- 每一个变量都有一个内存位置,每一个内存位置都定义了可使用
&
运算符访问的地址,它表示了在内存中的一个地址。 - 它的操作数必须是变量
2.2. *
的不同含义
2.2.1. 指针声明符
int *p = i;
2.2.2. 间接(寻址)运算符
- 也称解引用运算符(dereferencing operator)
- 是⼀个单目运算符
- 可以做右值也可以做左值
int k = *p;
*p = k+1;
2.3. 指针的运算符& *
的关系
- 互相反作用
*&yptr
->* (&yptr)
->* (yptr的地址)
-> 得到那个地址上的变量 ->yptr
&*yptr
->&(*yptr)
->&(y)
-> 得到y的地址,也就是yptr
->yptr
3. C 中的 NULL 指针
- 在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个
NULL
值是一个良好的编程习惯。赋为NULL
值的指针被称为空指针。
#include <stdio.h>
int main ()
{
int *ptr = NULL;
printf("ptr 的地址是 %p\n", ptr );
return 0;
}
ptr 的地址是 0x0
- 在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。
- 如需检查一个空指针,您可以使用 if 语句,如下所示:
if(ptr) /* 如果 p 非空,则完成 */
4. 指针与const
4.1. 指针是 const
- 表示一旦得到了某个变量的地址,不能再指向其他的变量
int* const q = &i;//q是const
*q = 26;//OK
q++;//ERROR
4.2. 所指是const
- 表示不能通过这个指针去修改那个变量(并不能使得那个变量成为
const
,且可以让指针指向别处)
const int *p = &i;
*p = 26; // ERROR! (*p) 是 const
i = 26; //OK
p = &j; //OK
p++;//OK
4.3. Others
int i;
const int* p1 = &i;
int const* p2 = &i;//相同
int* const p3 = &i;
- 判断哪个被
const
了的标志是const
在*
的前面还是后面 - 在创建指针时还可以使用
const
两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值
4.4. 转换
- 总是可以把一个非
const
的值转换成const
的
void f(const int* x);
int a = 15;
f(&a); // ok
const int b = a;
f(&b); // ok
b = a + 1; // Error!
- 当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字数传递值给参数,又能避免函数对外面的变量的修改
4.5. 对形式参数使用const
- 为了保护数组不被函数破坏,可以设置参数为
const
int sum(const int a[], int length);
- 这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改
4.6. const
的其他内容
- 只能把非
const
数据的地址赋给普通指针
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
double * pnc = rates; // 有效
pnc = locked; // 无效
pnc = &rates[3]; // 有效
- 这个规则非常合理。否则,通过指针就能改变
const
数组中的数据
5. C 指针详解
5.1. 指针的算术运算
- 常用于数组类的连续空间操作
- 在C语言中,指针的算术运算只包括两个相同类型的指针相减以及指针加上或减去一个整数
5.1.1. 递增一个指针
- 我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。
*
的优先级虽然高,但是没有后缀++
高
int arr[] = {5, 10, 15, 20, 25};
int *p = arr;
//------------输出结果是在单独执行下面代码的前提下----------------//
//printf("%d\n", *++p); // 10 p先自+,然后*p,最终为10
//printf("%d\n", ++*p); // 6 先*p,即arr[0]=1,然后再++,最终为6
//printf("%d\n", *p++); // 5 先执行p++(没自加成功前面有其他运算符,要先处理),再执行 *,p指向arr[1]
//printf("%d\n", (*p)++); // 5 先*p,即*p=arr[0]=1,然后1++,该一整句执行完毕后,在下一句代码执行前,arr[0] 会在原值基础上加1 = 6
//printf("%d\n", *(p++)); // 5 效果等同于*p++
- 下面的程序递增变量指针,以便顺序访问数组中的每一个元素:
#include <stdio.h>
const int MAX = 3;
int main ()
{
int var[] = {10, 100, 200};
int i, *ptr;
ptr = var;/* 指针中的数组地址 */
for ( i = 0; i < MAX; i++)
{
printf("存储地址:var[%d] = %p\n", i, ptr );
printf("存储值:var[%d] = %d\n", i, *ptr );
ptr++;/* 指向下一个位置 */
}
return 0;
}
存储地址:var[0] = e4a298cc
存储值:var[0] = 10
存储地址:var[1] = e4a298d0
存储值:var[1] = 100
存储地址:var[2] = e4a298d4
存储值:var[2] = 200
5.1.2. 递减一个指针
5.1.3. 指针的比较
- 下面的程序修改了上面的实例,只要变量指针所指向的地址小于或等于数组的最后一个元素的地址
&var[MAX - 1]
,则把变量指针进行递增:
#include <stdio.h>
const int MAX = 3;
int main ()
{
int var[] = {10, 100, 200};
int i, *ptr;
ptr = var; /* 指针中第一个元素的地址 */
i = 0;
while ( ptr <= &var[MAX - 1] )
{
printf("存储地址:var[%d] = %p\n", i, ptr );
printf("存储值:var[%d] = %d\n", i, *ptr );
/* 指向上一个位置 */
ptr++;
i++;
}
return 0;
}
存储地址:var[0] = 0x7ffeee2368cc
存储值:var[0] = 10
存储地址:var[1] = 0x7ffeee2368d0
存储值:var[1] = 100
存储地址:var[2] = 0x7ffeee2368d4
存储值:var[2] = 200
5.2. 指针数组
- 声明:
type *var_name[MAX]
#include <stdio.h>
const int MAX = 3;
int main ()
{
int var[] = {10, 100, 200};
int i, *ptr[MAX];
for ( i = 0; i < MAX; i++)
{
ptr[i] = &var[i]; /* 赋值为整数的地址 */
}
for ( i = 0; i < MAX; i++)
{
printf("Value of var[%d] = %d\n", i, *ptr[i] );
}
return 0;
}
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
- 也可以用一个指向字符的指针数组来存储一个字符串列表,如下:
#include <stdio.h>
const int MAX = 4;
int main ()
{
const char *names[] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
int i = 0;
for ( i = 0; i < MAX; i++)
{
printf("Value of names[%d] = %s\n", i, names[i] );
}
return 0;
}
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
5.3. 指向指针的指针
- 一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
- ![[指向指针的指针.jpg]]
- 声明:
type **var_name;
5.4. 传递指针给函数
- ![[C_09_数组#7.2. 传递数组给函数]]
5.5. 从函数返回指针
- 声明:
int * myFunction()
{
.
.
.
}
- C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为
static
变量。- 因为局部变量是存储在内存的栈区内,当函数调用结束后,局部变量所占的内存地址便被释放了,因此当其函数执行完毕后,函数内的变量便不再拥有那个内存地址,所以不能返回其指针。
- 除非将其变量定义为
static
变量,static
变量的值存放在内存中的静态数据区,不会随着函数执行的结束而被清除,故能返回其地址。
5.6. 指针的类型转换
- 指针也可以转换类型
int *p = &i; void *q = (void*)p;
5.7. 指针和多维数组
- ![[C_09_数组#7.1.3. 指针和多维数组]]
5.8. 指针的兼容性
int *pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; // 一个指向指针的指针
pt = &ar1[0][0]; // 都是指向int的指针
pt = ar1[0]; // 都是指向int的指针
pt = ar1; // 无效
pa = ar1; // 都是指向内含3个int类型元素数组的指针
pa = ar2; // 无效
p2 = &pt; // both pointer-to-int *
*p2 = ar2[0]; // 都是指向int的指针
p2 = ar2; // 无效
6. 指向函数的指针(函数指针)
- 函数指针可以像一般函数一样,用于调用函数、传递参数
- 声明:
typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
以下实例声明了函数指针变量 p,指向函数 max:
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int main(void)
{
/* p 是函数指针 */
int (* p)(int, int) = & max; // &可以省略
int a, b, c, d;
printf("请输入三个数字:");
scanf("%d %d %d", & a, & b, & c);
/* 与直接调用函数等价,d = max(max(a, b), c) */
d = p(p(a, b), c);
printf("最大的数字是: %d\n", d);
return 0;
}
请输入三个数字:1 2 3
最大的数字是: 3
6.1. 回调函数
- 回调函数就是一个通过函数指针调用的函数。简单讲:回调函数是由别人的函数执行时调用你实现的函数。
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
6.1.1. 实例
- 实例中
populate_array()
函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。实例中我们定义了回调函数getNextRandomValue()
,它返回一个随机值,它作为一个函数指针传递给populate_array()
函数populate_array()
将调用 10 次回调函数,并将回调函数的返回值赋值给数组。
#include <stdlib.h>
#include <stdio.h>
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
// 获取随机值
int getNextRandomValue(void)
{
return rand();
}
int main(void)
{
int myarray[10];
/* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
16807 282475249 1622650073 984943658 1144108930 470211272 101027544 1457850878 1458777923 2007237709
7. 指针应用
交换两个变量的值:
void swap(int *pa, int *pb){
int t = *pa;
*pa = *pb;
*pb = t;
}
- 函数返回多个值,某些值就只能通过指针返回
- 函数返回运算的状态,结果通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错
- -1或0(在文件操作会看到大量的例子)