C语言-第八章:指针进阶

传送门:C语言-第七章:字符和字符串函数、动态内存分配

目录

第一节:常见指针

1-1.字符指针

1-1-1.变量字符串

1-1-2.常量字符串

[1-1-2-1.const 关键字](#1-1-2-1.const 关键字)

第二节:指针数组

第三节:数组指针

第四节:函数指针

第五节:函数指针数组

下期预告:


第一节:常见指针

1-1.字符指针

字符指针就是指向字符串的指针

cpp 复制代码
char ch[]= "Hello world";
char* ptr = &ch;

本质是把字符串的首地址放在了 ptr 中:

cpp 复制代码
#include <stdio.h>

int main()
{
	char ch[] = "Hello world";
	char* ptr = &ch;
	printf("%p\n", ptr);
	return 0;
}

字符串也分为 变量字符串 和 常量字符串:

1-1-1.变量字符串

用字符数组定义的变量就是变量字符串,一般存储在栈上,以下就是一个变量字符串:

cpp 复制代码
char ch[] = "Hello world";

变量字符串的特点就是单独存储,即使定义两个内容相同的字符串,它也会另外开空间:

cpp 复制代码
#include <stdio.h>

int main()
{
	char ch1[] = "Hello world";
	char ch2[] = "Hello world";
	printf("%p\n", &ch1);
	printf("%p\n", &ch2);
	return 0;
}

常量字符串还有一个特点就是它可以改变内容:

cpp 复制代码
#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;
}

上述的代码是将字符串作为一个一个的字符进行改变,但是字符串无法作整体的改变(字符串具有常性,但是字符不具有常性):

cpp 复制代码
#include <stdio.h>

int main()
{
	char ch[] = "Hello world";
	ch = "ni hao shi jie"; // 报错,无法修改
	return 0;
}

1-1-2.常量字符串

常量字符串存储在常量区,它只能通过指针访问,这个指针还需要 const 来修饰:

cpp 复制代码
#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 在 * 之前:

这种方式表示指针所指向的空间存储的无法通过这个指针解引用改变:

cpp 复制代码
#include <stdio.h>

int main()
{
	int a = 0;
	const int* ptr = &a;
	*ptr = 1; // 无法改变
	return 0;
}
  1. const 在 * 之后:

可以通过指针修改指向的空间,但是指针存储的地址不能改变,也就是它的指向不能改变:

cpp 复制代码
#include <stdio.h>

int main()
{
	int a = 0;
	int b = 1;
	int* const ptr = &a;
	ptr = &b;
	return 0;
}

常量字符串具有常性,它的值无法改变,所以要用 const 修饰的指针接收,这是一种权限的平移,即权限没有改变,如果不用 const 修饰就是一种权限的扩大,这是不安全的。

常量字符串具有唯一性,如果指向的内容相同的 const 指针指向同一块空间:

cpp 复制代码
#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;
}

它的示意图如下:

除了用指针访问常量字符串,我们也可以直接使用常量字符串:

cpp 复制代码
#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" 就变成了它自己的首元素地址。

所以我们甚至可以用 [ ] 访问它的元素:

cpp 复制代码
#include <stdio.h>

int main()
{
	printf("%c\n", "Hello world"[0]);
	return 0;
}

常量字符串还具有常性,它的值是无法改变的:

cpp 复制代码
#include <stdio.h>

int main()
{
	const char* ptr = "Hello world";
	ptr[0] = 'h';
	return 0;
}

学习了变量字符串和常量字符串之后,请看以下代码,判断 ptr 和 ch 是常量字符串还是变量字符串:

cpp 复制代码
#include <stdio.h>

int main()
{
	const char ch[] = "Hello world";
	const char* ptr = ch;
	return 0;
}

ch 是一个变量字符串,ptr 也指向一个变量字符串,虽然 ch 用 const 修饰,但是它仍然属于局部变量,存储在栈上;ptr 也是局部变量,存储在栈上,而且它得到的地址是 ch 的首元素地址,它指向 ch。

我们可以让它们的地址与一个常量字符串的地址作比较:

cpp 复制代码
#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;
}

常量字符串的存储位置与其他位置差距大,这是因为栈区与常量区距离较远。

第二节:指针数组

指针数组是一种数组,但它的元素类型是指针。

cpp 复制代码
int* p_arr[3];

类比 int arr[3],int*是它的元素类型,p_arr 是数组名,[3] 是它的容量。

我们可以向里面放入指针或者地址:

cpp 复制代码
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:

cpp 复制代码
#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:

cpp 复制代码
#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;
}

二级指针的定义如下:

cpp 复制代码
int a = 0;
int* ptr = &a;
int** pptr = &ptr;

当然也有三级指针、四级指针,但是用途很小。

二级指针的一次解引用就是一级指针,可以对一级指针进行访问和修改,这种用法在链表、二叉树中常见。

第三节:数组指针

数组指针是一种指针,它指向一个数组。

cpp 复制代码
int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;

parr是它的指针名,(*parr) 表示 parr 的类型是个指针,int [5] 表示 parr 指向的数组类型,这个数组的元素类型是 int 数组容量是 5。

我们之前学过的二维数组名就是一个数组指针,因为二维数组名指向首元素,首元素又是个一维数组,即二维数组名指向一个一维数组,符合数组指针的概念。

我们可以用数组指针接收二维数组名:

cpp 复制代码
#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;
}

第四节:函数指针

函数指针也是一种指针,它指向一个函数,因为函数有返回值类型、形参类型,在定义函数指针时也需要体现出来:

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}
int (*pAdd)(int x, int y) = &Add;

定义函数指针时也可以忽略形参名,只写上类型:

cpp 复制代码
int (*pAdd)(int, int) = &Add;

C语言规定函数的名字就是指向这个函数指针,所以初始化时的 & 符号也可以省略,即 Add 与 &Add 是等价的:

cpp 复制代码
int (*pAdd)(int, int) = Add;

我们可以使用函数指针调用函数,用法和函数名一样,加 (参数) 调用:

cpp 复制代码
#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)的函数:

cpp 复制代码
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 也能接收,但是调用时很可能出现问题,要避免这种情况的发生。

函数指针的类型也可以作为函数的返回值的类型,请看以下这种写法:

cpp 复制代码
int Add(int x, int y)
{
	return x + y;
}
int(*)(int, int) returnFunPtr() // 错误写法
{
	return Add;
}

直接把 int(*)(int, int) 写到返回类型的位置是不行的,编译器会不认识它,它实际上要这样写:

cpp 复制代码
int(*returnFunPtr())(int, int) // 正确写法
{
	return Add;
}

即把他自己的函数名和参数放到 (*) 中,这样的写法不仅不符合常识,代码的可读性也不好, 为了解决这个问题,我们需要用到 typedef 类型重定义关键字,它的作用就是给一个类型起一个别名,例如给之前讲过的基本类型取别名:

cpp 复制代码
typedef int INT;
typedef char CHAR;
typedef long long DLONG;
typedef float FLOAT;

别名就等价于原名,可以用它定义的变量的类型和原名定义的变量类型是一样的:

cpp 复制代码
INT a; // 等价于 int a
sizeof(int); // 等价于 sizeof(int) 

但是函数类型的起别名方式有点不同,要把别名放在括号中,而不是原名的后面:

cpp 复制代码
typedef int(*FuncName)(int,int);

上述代码中的 FuncName 就是 int(*)(int,int) 的别名,它可以直接放在返回值类型的位置:

cpp 复制代码
typedef int(*FuncName)(int, int);
int Add(int x, int y)
{
	return x + y;
}
FuncName returnFunPtr()
{
	return Add;
}

此时调用 returnFunPtr 我们就可以得到 Add 函数的地址:

cpp 复制代码
#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) 的函数的类型又是什么呢?我们可以用下列两种写法不同,但是类型相同的指针接收:

cpp 复制代码
FuncName(*pRFP1)() = returnFunPtr; // 有typedef
int(*(*pRFP2)())(int, int) = returnFunPtr; // 无typedef

无 typedef 版本的写法也要把指针名放在 (*) 里面,其中最里面的 * 表示 pRFP2 的类型是个指针,空的()是 returnFunPtr 的参数,外层的 int(*)(int,int) 是 returnFunPtr 的返回值类型。

确实很复杂,所以还是尽量采用第一种写法。

第五节:函数指针数组

函数指针数组是存放函数指针的数组,函数指针的类型必须相同:

cpp 复制代码
int(*ptr_func_arr[4])(int,int);

解析:

在这里有一个便捷的方法,就是把函数类型看作一个整体,放到它"本来"的位置,然后把其他部分放到 (*) 中:

几乎所有包含函数类型的部分都可以这样做。

当然也可以用 typedef,让它更符合我们的"审美":

cpp 复制代码
typedef int(*FuncName)(int, int); // 函数类型重定义
FuncName ptr_func_arr[4];

使用函数指针数组可以让我们方便的调用功能相似的函数,比如写一个简单的整数计算器:

cpp 复制代码
#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语言-第九章:文件读写

相关推荐
朱一头zcy5 分钟前
C语言复习第9章 字符串/字符/内存函数
c语言
此生只爱蛋8 分钟前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
blammmp15 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧28 分钟前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
暗黑起源喵34 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong38 分钟前
Java反射
java·开发语言·反射
Troc_wangpeng40 分钟前
R language 关于二维平面直角坐标系的制作
开发语言·机器学习
努力的家伙是不讨厌的41 分钟前
解析json导出csv或者直接入库
开发语言·python·json
Envyᥫᩣ1 小时前
C#语言:从入门到精通
开发语言·c#
童先生1 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go