目录
第一节:常见指针
1-1.字符指针
字符指针就是指向字符串的指针
char ch[]= "Hello world";
char* ptr = &ch;
本质是把字符串的首地址放在了 ptr 中:
#include <stdio.h>
int main()
{
char ch[] = "Hello world";
char* ptr = &ch;
printf("%p\n", ptr);
return 0;
}
字符串也分为 变量字符串 和 常量字符串:
1-1-1.变量字符串
用字符数组定义的变量就是变量字符串,一般存储在栈上,以下就是一个变量字符串:
char ch[] = "Hello world";
变量字符串的特点就是单独存储,即使定义两个内容相同的字符串,它也会另外开空间:
#include <stdio.h>
int main()
{
char ch1[] = "Hello world";
char ch2[] = "Hello world";
printf("%p\n", &ch1);
printf("%p\n", &ch2);
return 0;
}
常量字符串还有一个特点就是它可以改变内容:
#include <stdio.h>
int main()
{
char ch[] = "Hello world";
for (int i = 0; i < sizeof(ch)-1; i++)
{
ch[i] = 'a';
}
printf("%s\n", ch);
return 0;
}
上述的代码是将字符串作为一个一个的字符进行改变,但是字符串无法作整体的改变(字符串具有常性,但是字符不具有常性):
#include <stdio.h>
int main()
{
char ch[] = "Hello world";
ch = "ni hao shi jie"; // 报错,无法修改
return 0;
}
1-1-2.常量字符串
常量字符串存储在常量区,它只能通过指针访问,这个指针还需要 const 来修饰:
#include <stdio.h>
int main()
{
const char* ch = "Hello world";
printf("%p\n", ch);
return 0;
}
1-1-2-1.const 关键字
const 意味永久的、不可改变的,用它修饰的变量在初始化后无法改变其值,而 const 修饰指针有两种用法:
1. const 在 * 之前:
这种方式表示指针所指向的空间存储的无法通过这个指针解引用改变:
#include <stdio.h>
int main()
{
int a = 0;
const int* ptr = &a;
*ptr = 1; // 无法改变
return 0;
}
2. const 在 * 之后:
可以通过指针修改指向的空间,但是指针存储的地址不能改变,也就是它的指向不能改变:
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
int* const ptr = &a;
ptr = &b;
return 0;
}
常量字符串具有常性,它的值无法改变,所以要用 const 修饰的指针接收,这是一种权限的平移,即权限没有改变,如果不用 const 修饰就是一种权限的扩大,这是不安全的。
常量字符串具有唯一性,如果指向的内容相同的 const 指针指向同一块空间:
#include <stdio.h>
int main()
{
const char* ptr1 = "Hello world";
const char* ptr2 = "Hello world";
printf("%p\n", ptr1);
printf("%p\n", ptr2);
return 0;
}
它的示意图如下:
除了用指针访问常量字符串,我们也可以直接使用常量字符串:
#include <stdio.h>
int main()
{
const char* ptr1 = "Hello world";
const char* ptr2 = "Hello world";
printf("%p\n", ptr1);
printf("%p\n", ptr2);
printf("%p\n", "Hello world"); // 直接使用常量字符串
return 0;
}
为什么可以直接得到它的地址呢?这是因为字符串存储时,它的名字就存储了自己的首元素地址,如果直接使用 "Hello world" ,在代码的编译阶段 "Hello world" 就变成了它自己的首元素地址。
所以我们甚至可以用 [ ] 访问它的元素:
#include <stdio.h>
int main()
{
printf("%c\n", "Hello world"[0]);
return 0;
}
常量字符串还具有常性,它的值是无法改变的:
#include <stdio.h>
int main()
{
const char* ptr = "Hello world";
ptr[0] = 'h';
return 0;
}
学习了变量字符串和常量字符串之后,请看以下代码,判断 ptr 和 ch 是常量字符串还是变量字符串:
#include <stdio.h>
int main()
{
const char ch[] = "Hello world";
const char* ptr = ch;
return 0;
}
ch 是一个变量字符串,ptr 也指向一个变量字符串,虽然 ch 用 const 修饰,但是它仍然属于局部变量,存储在栈上;ptr 也是局部变量,存储在栈上,而且它得到的地址是 ch 的首元素地址,它指向 ch。
我们可以让它们的地址与一个常量字符串的地址作比较:
#include <stdio.h>
int main()
{
const char ch[] = "Hello world";
const char* ptr = ch;
printf("变量字符串存储位置:%p\n", ch);
printf("指针的存储位置:%p,指针指向的地址:%p\n", &ptr,ptr);
printf("常量字符串存储位置:%p\n", "Hello world");
return 0;
}
常量字符串的存储位置与其他位置差距大,这是因为栈区与常量区距离较远。
第二节:指针数组
指针数组是一种数组,但它的元素类型是指针。
int* p_arr[3];
类比 int arr[3],int*是它的元素类型,p_arr 是数组名,[3] 是它的容量。
我们可以向里面放入指针或者地址:
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* p_arr[] = {&a,&b,&c};
return 0;
}
那么 p_arr[0] 是什么呢?它就是数组中第一个元素,也是指向 a 的指针,类型是 int*,可以解引用访问和修改a:
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* p_arr[] = {&a,&b,&c};
printf("%d\n", * p_arr[0]);
return 0;
}
我们知道数组名存储的是首元素的地址,可以认为数组名是指向首素的指针,然后指针数组的首元素又是一个指针,它们的指向关系是:
像这种数组名间接指向 a 的指针叫做二级指针,两次解引用就可以访问到a:
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* p_arr[] = {&a,&b,&c};
int** pptr = p_arr; // 可以用二级指针接收指针数组名
printf("%d\n", **p_arr);
printf("%d\n", **pptr);
return 0;
}
二级指针的定义如下:
int a = 0;
int* ptr = &a;
int** pptr = &ptr;
当然也有三级指针、四级指针,但是用途很小。
二级指针的一次解引用就是一级指针,可以对一级指针进行访问和修改,这种用法在链表、二叉树中常见。
第三节:数组指针
数组指针是一种指针,它指向一个数组。
int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;
parr是它的指针名,(*parr) 表示 parr 的类型是个指针,int [5] 表示 parr 指向的数组类型,这个数组的元素类型是 int 数组容量是 5。
我们之前学过的二维数组名就是一个数组指针,因为二维数组名指向首元素,首元素又是个一维数组,即二维数组名指向一个一维数组,符合数组指针的概念。
我们可以用数组指针接收二维数组名:
#include <stdio.h>
int main()
{
int arr[][3] = { {1,2,3},{4,5,6},{7,8,9} };
int(*parr)[3] = &arr;
printf("%d\n", arr[0][0]);
printf("%d\n", (*parr)[0]);
return 0;
}
第四节:函数指针
函数指针也是一种指针,它指向一个函数,因为函数有返回值类型、形参类型,在定义函数指针时也需要体现出来:
int Add(int x, int y)
{
return x + y;
}
int (*pAdd)(int x, int y) = &Add;
定义函数指针时也可以忽略形参名,只写上类型:
int (*pAdd)(int, int) = &Add;
C语言规定函数的名字就是指向这个函数指针,所以初始化时的 & 符号也可以省略,即 Add 与 &Add 是等价的:
int (*pAdd)(int, int) = Add;
我们可以使用函数指针调用函数,用法和函数名一样,加 (参数) 调用:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pAdd)(int x, int y) = &Add;
int ret = pAdd(1,2); // 也可以是(*pAdd)(1,2)
printf("%d\n",ret);
return 0;
}
pAdd 的类型是 int(*)(int,int),这意味着它能接收其他返回值类型为 int ,参数为(int,int)的函数:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int (*pAdd)(int x, int y) = &Add;
int ret = pAdd(1,2);
printf("%d\n", ret);
pAdd = Sub; // 函数指针重新赋值
ret = pAdd(2,1);
printf("%d\n", ret);
return 0;
}
如果函数的类型不同,pAdd 也能接收,但是调用时很可能出现问题,要避免这种情况的发生。
函数指针的类型也可以作为函数的返回值的类型,请看以下这种写法:
int Add(int x, int y)
{
return x + y;
}
int(*)(int, int) returnFunPtr() // 错误写法
{
return Add;
}
直接把 int(*)(int, int) 写到返回类型的位置是不行的,编译器会不认识它,它实际上要这样写:
int(*returnFunPtr())(int, int) // 正确写法
{
return Add;
}
即把他自己的函数名和参数放到 (*) 中,这样的写法不仅不符合常识,代码的可读性也不好, 为了解决这个问题,我们需要用到 typedef 类型重定义关键字,它的作用就是给一个类型起一个别名,例如给之前讲过的基本类型取别名:
typedef int INT;
typedef char CHAR;
typedef long long DLONG;
typedef float FLOAT;
别名就等价于原名,可以用它定义的变量的类型和原名定义的变量类型是一样的:
INT a; // 等价于 int a
sizeof(int); // 等价于 sizeof(int)
但是函数类型的起别名方式有点不同,要把别名放在括号中,而不是原名的后面:
typedef int(*FuncName)(int,int);
上述代码中的 FuncName 就是 int(*)(int,int) 的别名,它可以直接放在返回值类型的位置:
typedef int(*FuncName)(int, int);
int Add(int x, int y)
{
return x + y;
}
FuncName returnFunPtr()
{
return Add;
}
此时调用 returnFunPtr 我们就可以得到 Add 函数的地址:
#include <stdio.h>
typedef int(*FuncName)(int, int);
int Add(int x, int y)
{
return x + y;
}
FuncName returnFunPtr()
{
return Add;
}
int main()
{
int (*pAdd)(int, int) = returnFunPtr(); // 接收Add函数的地址
int ret = pAdd(1,2);
printf("%d\n", ret);
return 0;
}
那么这个返回类型为 int(*)(int,int) 的函数的类型又是什么呢?我们可以用下列两种写法不同,但是类型相同的指针接收:
FuncName(*pRFP1)() = returnFunPtr; // 有typedef
int(*(*pRFP2)())(int, int) = returnFunPtr; // 无typedef
无 typedef 版本的写法也要把指针名放在 (*) 里面,其中最里面的 * 表示 pRFP2 的类型是个指针,空的()是 returnFunPtr 的参数,外层的 int(*)(int,int) 是 returnFunPtr 的返回值类型。
确实很复杂,所以还是尽量采用第一种写法。
第五节:函数指针数组
函数指针数组是存放函数指针的数组,函数指针的类型必须相同:
int(*ptr_func_arr[4])(int,int);
解析:
在这里有一个便捷的方法,就是把函数类型看作一个整体,放到它“本来”的位置,然后把其他部分放到 (*) 中:
几乎所有包含函数类型的部分都可以这样做。
当然也可以用 typedef,让它更符合我们的“审美”:
typedef int(*FuncName)(int, int); // 函数类型重定义
FuncName ptr_func_arr[4];
使用函数指针数组可以让我们方便的调用功能相似的函数,比如写一个简单的整数计算器:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
typedef int(*FuncName)(int, int); // 函数类型重定义
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
if (y == 0) // 避免除0错误
{
return 0;
}
return x / y;
}
int main()
{
FuncName ptr_func_arr[] = {Add, Sub, Mul, Div};
int x, y;
char oper;
while (1)
{
printf("Please enter:");
scanf("%d %c %d", &x, &oper, &y);
switch (oper)
{
case '+':
printf("%d\n", ptr_func_arr[0](x, y));
break;
case '-':
printf("%d\n", ptr_func_arr[1](x, y));
break;
case '*':
printf("%d\n", ptr_func_arr[2](x, y));
break;
case '/':
printf("%d\n", ptr_func_arr[3](x, y));
break;
}
}
return 0;
}
下期预告:
下一次是文件相关操作,主要是文件的打开、关闭,文件的读写操作的函数
传送门:C语言-第九章:文件读写