C++:征服C指针:指针(一)


本章节,我们重点分析C指针,首先我们从指针的概念谈起
💚💚💚

  1. 指针是一种保存变量地址的变量,并在C中频繁使用
  2. 在C语言标准中:最初出现指针,也有这样一段话
    🧡指针类型(pointer type)可由:函数类型,对象类型或不完全的类型派生,派生指针类型的类型称为引用类型。
    🧡指针类型,描述一个对象,该类对象的值提供对该引用类型实体的引用。由引用类型T 派生的指针类型有时称为 “指向T的指针”
    从引用类型构造指针类型的过程称为“指针类型的派生”。
    🧡 如由 int 类型派生的指针,称为 “指向int 类型的指针”。

下面,我们对指针的研究,主要从如下几个方面展开。
3. 指针类型
4. 指针类型变量
5. 指针类型的值(即内存地址)

1.看一个简单的程序,来接触下指针

#include<iostream>
using namespace std;
int main()
{
	int hoge = 5;
	int piyo = 10;
	int* hoge_p;
	
	// 输出每个变量的地址
	printf("&hoge...%p\n",&hoge);
	printf("&piyo...%p\n",&piyo);
	printf("&hoge_p...%p\n",&hoge_p);
	
	// 将 hoge的内存地址赋值给 hoge_p
	hoge_p = &hoge;
	// 打印指针变量的值
	printf("hoge_p...%p\n",hoge_p);
	
	// 通过hoge_p输出 hoge的内容
	printf("*hoge_p...%d\n",*hoge_p);
	
	// 通过 hoge_p修改 hoge的内容
	*hoge_p = 10;
	printf("*hoge...%d\n",hoge);
	return 0;
}

// 打印内容
&hoge...000000000061fe1c
&piyo...000000000061fe18
&hoge_p...000000000061fe10

hoge_p...000000000061fe1c
*hoge_p...5
*hoge...10

我们用一个图来说明下。
在这里插入图片描述

  1. 指针变量 hoge_p保存了另外一个变量 hoge地址,我们认为 “hoge_p” 指向 “hoge”。
  2. 对 hoge变量实施 &运算符得到 “hoge地址” 。 有时也称:"hoge的地址"的为 “指向 hoge的指针” (实际上这里的指针指:指针类型的值)
  3. 在 指针前面加上 * 可以表示指针指向的变量。(hoge_p 指向hoge), 所以 , *hoge_p 等同于 hoge , 那么一旦要求输出 *hoge_p 就会输出 hoge保存的值 。

🧡🧡🧡 要点

  1. 对变量使用 & 运算符,可以取得该变量的地址。 这个地址称为指向 该变量的指针。
  2. 指针变量 hoge_p 保存了指向其他变量的地址情况下(如保存 hoge 地址)可以说 “hoge_p 指向 hoge” 。
  3. 对指针变量运算 *运算符,就等同于它指向的变量。(如 hoge_p指向 hoge, *hoge_p就等同于 hoge)。

2. 常见疑问:指针就是地址,那么int的指针和double的指针有什么区别 了

  1. 如果从指针变量的角度说,指向这两个类型的指针没有区别,都保持相同的表现形式,与指针类型无关。
    💚(尽管C标准没有规定所有数据类型的长度,但通常是这样一种情况:整数类型指针长度是一样的, char 类型指针和 结构体指针 长度一致,函数指针长度与数据指针长度不同)
    💚 指针长度取决于使用的机器和编译器,通常在 现代 windows上,指针是32 位或64位长,对于DOS来说是 16位长。
  2. 从指针运算的角度来说,就需要关注指针类型了
    💚(如:对指针加 N 即 :指针前进 “当前指针指向的数据类型的长度 * N”)
  3. 指向任何类型的指针类型 ------ void* 类型
void test_voidPointer()
{
	int hoge = 5;
	void* hoge_p;
	// void 类型指针保存 hoge内存地址,这个是没问题的
	hoge_p = &hoge;
	
	// 但是由于编译器并不知道 hoge_p 指针类型,仅仅知道内存地址,不知道保存数据的类型,这样是不能取值的。
	printf("%d", *hoge_p);  // error :'void*' is not a pointer-to-object type
}

💚💚💚 改正

void test_pointer_02()
{
	int hoge = 5;
	void* hoge_p;
	// void 类型指针保存 hoge内存地址,这个是没问题的
	hoge_p = &hoge;
	
	// 将指针强制转换成 指向 int 类型指针,这样编译器就指针取出来的是 int 类型的值
	printf(" %d",*(int*)hoge_p);  // 打印5
}

3. 常见疑问:指针运算

  1. C指针运算是其他语言没有的
  2. 指针运算是针对指针进行整数加减运算
// 指针的运算
void test_pointer_03(){
	int hoge;
	int *hoge_p;
	// 将指向 hoge的指针赋予 hoge_p
	hoge_p = &hoge;
	// 输出 hoge_p的值
	printf("hoge_p...%p\n",hoge_p);
	// 给 hoge_p 加1
	hoge_p++;
	// 输出 hoge_p 的值
	printf("hoge_p...%p\n",hoge_p);
	// 输出 hoge_p后 加3 的值
	printf("hoge_p....%p\n",hoge_p +3);
}
// 打印结果
hoge_p...000000000061fde4
hoge_p...000000000061fde8
hoge_p....000000000061fdf4

🧡从上面的打印可知:指针加1 ,前进的字节是4个。这个结论和简单,也很直观。但是,我想通过:指针和数组的关系来证明下这个结论。请看下面示例

// 指针和数组之间关系
void test_pointer_04()
{
	int array[5];
	int *p;
	int i;
	// 给数组 array 的各元素设定值
	for(i=0; i<5;i++)
	{
		array[i] = i;
	}
	// 输出数组各元素的值(指针版本)
	for(p = &array[0]; p != &array[5]; p++)
	{
		printf("%d\n",*p);
	}
}

// 打印结果
0
1
2
3
4	

在这里插入图片描述
💚💚💚

4. 为什么存在奇怪的指针运算符

在第三章节中,我们知道访问数组的内容,老老实实用下标就可以了,为什么C语言需要存在指针运算符这样奇怪的功能了 ?

  1. C继承了 早期的 B 语言影响
  2. 使用指针运算可以写出高效的程序。
    在这里插入图片描述
    ▲●通过角标的方式访问数组,array[i] 在循环中会出现多次,每次都要进行相当于 *(array + i) 的加法运算,效率自然是比较低的
    ▲●但是通过 p+i 的方式,加法运算只有在循环结束的时候执行一次。

💚 如今,编译器在不断被优化,对于循环内部重复的表达式会集中处理,是编译器优化的基本内容,对于现在一般的C编译器,无论你使用的数组角标还是指针来访问 数组元素,效率上都不会出现明显的差距,基本删都是输出完整的机器码。

5. 试图将数组作为函数的参数进行传递。

从上几章节我们知道:
**💚 数组 buf[ ] 可以解读成 :

  1. 指向它的初始元素的指针
  2. *buf[ len] 是 (buf + len) 的语法糖
    下面,我们来看一个实用性的例子:从英文的文本中将单词 一个一个取出来 。
// 试图将数组作为函数参数来传递
void test_pointer_05(char* buf, int buf_size)
{
	
	
}
int main()
{
    char buf[256];
	test_pointer_05(buf,256);
	return 0;
}

6. 什么是空指针

💚 空指针是一个特殊的值

  1. 空指针是指可以确保没有指向任何一个对象的指针,通常使用 宏定义 NULL 来表示空指针常量值
  2. 空指针确保它和任何非空指针进行比较都不会相等,因此经常作为函数发生异常时的返回值使用
  3. 在现今的操作系统下,应用程序一旦视图通过空指针引用对象,就会马上招致一个异常并且当前应用程序会被操作系统强制终止

💚 常量0 和 NULL 的关系 。
4. 在C中,在为0的地址上,是不能保存数据的,放什么都不能起作用。所以标准运行 将 NULL 定义成 ( void*)0

int* p = 3; // error :invalid conversion from ‘int’ to ‘int*’
这是因为:编译器会认为 3 是int 类型,但是 p 指针 int* 指针类型,int类型和 指针类型肯定是有区别的

int* p1 = 0; // OK
std::cout <<*p1; // Segmentation fault
这是因为编译器根据上下文 “将常量0应该作为指针使用” , 这个时候编译是可以通过的,但是在运行时,获取指针类型保存的地址指向的值 是无效的,所以会出现 Segmentation fault

5.1 声明函数形参的方法

6. 指向函数的指针

💚 函数在表达式中被解读成 “指向函数的指针” , 它本质上也是指针(地址) ,所以可以将它赋给指针型变量 。

下面我们用一个图分析下,指向函数的 指针和 指向函数的指针的数组。

在这里插入图片描述
💚 看下面一个 指向函数的指针

void (* signal (int sig , void (*func)( int )) )(int );

6.1 函数指针具体使用示例

6.1.1 函数指针赋值

int (*func_01)(int a,int b);
int sum(int a, int b){
	return a+b;
}
void test_function_pointer()
{
	func_01 = sum; // 将sum函数地址赋值给 函数指针
	printf("sum: %d",func_01(1,2));  // 输出3
}

6.1.2 函数指针指向不同的函数

int (*func_01)(int a,int b);
int sum(int a, int b){
	return a+b;
}

int multiply(int a, int b){
	return a*b;
}

void test_function_pointer()
{
	// 将sum函数地址赋值给 函数指针
	func_01 = sum; 
	printf("sum: %d",func_01(1,2));  // 输出3
	
	// 同时也可以将 multiply 函数地址赋值给 函数指针
	func_01 = multiply;
	printf("\nmultiply: %d",func_01(1,2)); // 输出 2
}

6.1.3: 函数指针充当形参

我们通过常见的冒泡排序算法,来学习一下函数指针充当形参的用法

// -------------函数指针充当形参 start, 冒泡排序------------------------------

// 升序
bool descend(int a, int b)
{
	return a>b ? true:false;
}

// 降序
bool aescend(int a, int b)
{
	return a>b ? false:true;
}

void bubble_sort(int arr[],int size, bool (*complete)(int,int))
{
	int temp;
	for(int i =0; i<size-1; i++)
	{
		for(int j = i+1;j<size;j++)
		{
			if (complete(arr[i],arr[j]))
			{
				temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
	
}

void test_bubble(){
	int arr[] = {1,5,6,0,35,85,410};
	int arr_size = sizeof(arr)/sizeof(arr[0]);
	bubble_sort(arr,arr_size,aescend);
	for(int i = 0; i< arr_size;i++)
	{
		printf("%d:\n",arr[i]);
	}
}
// -------------函数指针充当形参 end, 冒泡排序------------------------------

6.1.4:函数指针用于回调函数

把一段代码,像传递参数一样传递给其他code,而这段代码在合适的时机将会被调用,这种情况就被称为回调。像我们6.1.3讲的冒泡排序以及快排中的comp,本质都属于回调。
💚💚💚 示例:
一个GPRS模块联网的小项目,使用过的同学大概知道2G、4G、NB等模块要想实现无线联网功能都需要经历模块上电初始化、注册网络、查询网络信息质量、连接服务器等步骤,这里的的例子就是,利用一个状态机函数(根据不同状态依次调用不同实现方法的函数),通过回调函数的方式依次调用不同的函数,实现模块联网功能。

// 工作状态
typedef struct{
	uint8_t mStatus;
	uint8_t (*Function)(void); // 函数指针
}M26_WorkStatus_Typedef;  //M26 状态机

// M26工作状态集合
M26_WorkStatus_Typedef M26_WorkStatus_Table[] = 
{
	{GPRS_NETWORK_CLOSE,  M26_PWRKEY_Off  }, //模块关机
    {GPRS_NETWORK_OPEN,  M26_PWRKEY_On  }, //模块开机
    {GPRS_NETWORK_Start,   M26_Work_Init  }, //管脚初始化
    {GPRS_NETWORK_CONF,  M26_NET_Config  }, /AT指令配置
    {GPRS_NETWORK_LINK_CTC,  M26_LINK_CTC  }, //连接调度中心  
    {GPRS_NETWORK_WAIT_CTC, M26_WAIT_CTC  },  //等待调度中心回复 
    {GPRS_NETWORK_LINK_FEM, M26_LINK_FEM  }, //连接前置机
    {GPRS_NETWORK_WAIT_FEM, M26_WAIT_FEM  }, //等待前置机回复
    {GPRS_NETWORK_COMM,  M26_COMM   }, //正常工作    
    {GPRS_NETWORK_WAIT_Sig,  M26_WAIT_Sig  },  //等待信号回复
    {GPRS_NETWORK_GetSignal,  M26_GetSignal  }, //获取信号值
    {GPRS_NETWORK_RESTART,  M26_RESET   }, //模块重启
}

// M26根据工作状态,回调上述状态函数
uint8_t call_M26_Workstatus(uint8_t state)
{
    uint8_t  i = 0;
	for(i = 0; i< 12; i++)
	{
		if(state == M26_WorkStatus_Table[i].mStatus) {
        	M26_WorkStatus_Table[i].Function(); // 回调对应的函数
        }
	}
}

6.2 函数返回值为指针类型

nt *fun(int x,int y);
这个函数就是一个指针函数。其返回值是一个 int 类型的指针,是一个地址。

7. 什么是指向数组的指针

  1. “数组”和“指针”都是派生类型,他们都是由基本类型开始重复派生生成的。也就是说派生出“数组”后,再派生出“指针”,就可以生成“指向数组的指针”。
  2. 我们需要区分:指向数组的指针和 指针数组初始元素的指针
// 指向数组的指针
void test_pointer_06()
{
	// array_p 是指向int 数组(元素个数为3个)的指针
	int (*array_p)[3]; 
	// p_array 是指向数组初始元素的指针
	int array[4];
	int *p_array = &array[0];
}

7.1 将数组类型解读成指针

💚 单目运算符& 被称为地址运算符 : &将一个左值作为操作数,返回指向该左值的指针
💚 单目运算符* 被称为解引用:将指针作为操作数,返回指针所指向的对象或函数。
💚 ->运算符:此运算符没有明确的定义,但可以将其称为“箭头运算符”,通过指针访问结构体成员时候,就会使用 ->运算符
💚 [ ] 下标运算符:在C语言中,遇导下标运算符[ ] 可以将元素个数省略不写,但是不同 编译器针对不同的情况,有不同的解释。

void test(){
	int hoge[10]{1,2,3,4,5,6,7,8,9,10}; // 这是一个数组
	// & 运算符
	int *p = &hoge[0]; // 将数组解读成指针
	// * 运算符
	int hoge_first = *p;  // 将指针解读成数组的值
	cout << hoge_first;  // 1
	
	// [] 小标运算符
	int hoge_second = hoge[1];
	int hoge_third = *(p+2);
	cout << "\n" << hoge_second;
	cout << "\n" << hoge_third;
	
	// ->运算符
	hoge_struct* hogeStru_p = &hogeStru;
	cout<< "\n"<< hogeStru_p->hoge;
	cout<< "\n"<< (*hogeStru_p).hoge;
}
int main()
{
	test();
	return 1;
}

💚下标运算符使用场景
在这里插入图片描述

8. const 修饰指针(常量指针/指针常量)

9. typedef 给指针定义别名

10. 指针和字符串常量

💚 使用 " " 包围起来的字符串被称为 字符串常量,字符串常量的类型是 " char的数组",因此在表达式中,可以被解读为指针 。

// 声明一个: 指向 char 类型的指针 str
char *str;
// 将 [指向 “abc” 的初始化元素的指针] 赋值给 str
str = "abc";

// note :  针对 字符串常量“abc” ,编译器事实上会将字符分开处理 即:
"abc" -----{'a','b','c','\0'}

void test_pointer_09(){
	char str[10];
	//  error: incompatible types in assignment of 'const char [4]' to 'char [10]'
	// str = "abc";
	strcpy(str,"abc");
	cout<< "str: " <<str;  // abc 
	printf("\nstr: %p: ",str); // 000000000061fde6
	printf("\nstr: %s ",str); // abc
	
}

11. 关于指向函数指针引起的混乱

💚

附录:代码示例

/**
	1: 指针: 指针类型,指针变量,指针变量的值
	2:指针就是存储的内存地址的
	3:将内存分区和指针结合起来
	4:void 指针,NULL指针
*/


#include<iostream>
#include<cstring>
using namespace std;

// 指针的基本操作:定义和概念
void test_pointer_01()
{
	
	int hoge = 5;
	int piyo = 10;
	int* hoge_p;
	
	// 输出每个变量的地址
	printf("&hoge...%p\n",&hoge);
	printf("&piyo...%p\n",&piyo);
	printf("&hoge_p...%p\n",&hoge_p);
	
	// 将 hoge的内存地址赋值给 hoge_p
	hoge_p = &hoge;
	// 打印指针变量的值
	printf("hoge_p...%p\n",hoge_p);
	
	// 通过hoge_p输出 hoge的内容
	printf("*hoge_p...%d\n",*hoge_p);
	
	// 通过 hoge_p修改 hoge的内容
	*hoge_p = 10;
	printf("*hoge...%d\n",hoge);
}

// 指针的转换
void test_pointer_02()
{
	int hoge = 5;
	void* hoge_p;
	// void 类型指针保存 hoge内存地址,这个是没问题的
	hoge_p = &hoge;
	
	// 但是由于编译器并不知道 hoge_p 指针类型,仅仅知道内存地址,不知道保存数据的类型,这样是不能取值的。
	// printf("%d",*hoge_p);  // error :invalid operands of types 'const char [4]' and 'void*' to binary 'operator*'
	
	// 将指针强制转换成 指向 int 类型指针
	printf(" %d",*(int*)hoge_p);  // 打印5
}

// 指针的运算
void test_pointer_03(){
	int hoge;
	int *hoge_p;
	// 将指向 hoge的指针赋予 hoge_p
	hoge_p = &hoge;
	// 输出 hoge_p的值
	printf("hoge_p...%p\n",hoge_p);
	// 给 hoge_p 加1
	hoge_p++;
	// 输出 hoge_p 的值
	printf("hoge_p...%p\n",hoge_p);
	// 输出 hoge_p后 加3 的值
	printf("hoge_p....%p\n",hoge_p +3);
}

// 指针和数组之间关系
void test_pointer_04()
{
	int array[5];
	int *p;
	int i;
	// 给数组 array 的各元素设定值
	for(i=0; i<5;i++)
	{
		array[i] = i;
	}
	// 输出数组各元素的值(指针版本)
	for(p = &array[0]; p != &array[5]; p++)
	{
		printf("%d\n",*p);
	}
	
}

// 试图将数组作为函数参数来传递
void test_pointer_05(char* buf, int buf_size)
{
	
	//int* p = 3; // error :invalid conversion from 'int' to 'int*'
	int* p1 = 0; 
	std::cout <<*p1; // Segmentation fault

}

// 指向数组的指针
void test_pointer_06()
{
	// array_p 是指向int 数组(元素个数为3个)的指针
	int (*array_p)[3]; 
	
	// p_array 是指向数组初始元素的指针
	int array[4];
	int *p_array = &array[0];
	cout<< "array[0] address: " << &array[0];
	cout<< "\np_array address: " << p_array;
	
	int p1_array[3];
	// array_p = p1_array; //  error cannot convert 'int [3]' to 'int (*)[3] :数组类型不能赋值给指针类型
	array_p = &p1_array;
	cout<< "\np1_array address: " << &p1_array;
	cout<< "\narray_p address: " << array_p;
}

// 指针数组
void test_pointer_06_01()
{
	int (*p[5])[2];
	int element[2] = {1, 2};
	p[0] = &element;

	// 输出element 数组元素
	printf("element[0]: %d\n", (*p[0])[0]);  //1
	printf("element[1]: %d\n", (*p[0])[1]);  // 2 
	printf("p[0][0] address: %p\n", p[0][0]);  
	printf("element[0] address: %p\n", &element[0]);  
	printf("p[0][1] address: %p\n", p[0][1]);  
	printf("element[1] address: %p\n", &element[1]);  

	
	int element_2[2] = {3, 4};
	p[1] = &element_2;
	
	// element_2 数组元素
	printf("\n\nelement_2[0]: %d\n", (*p[1])[0]);  //3
	printf("element_2[1]: %d\n", (*p[1])[1]);  // 4
	printf("p[1][0] address: %p\n", p[1][0]);  
	printf("element_2[0] address: %p\n", &element_2[0]);  
	printf("p[1][1] address: %p\n", p[1][1]);  
	printf("element_2[1] address: %p\n", &element_2[1]);  
}

struct hoge_struct{
	int hoge = 10;
}hogeStru;

// 运算符
void test_pointer_07(){
		
	int hoge[10]{1,2,3,4,5,6,7,8,9,10}; // 这是一个数组
	// & 运算符
	int *p = &hoge[0]; // 将数组解读成指针
	// * 运算符
	int hoge_first = *p;  // 将指针解读成数组的值
	cout << hoge_first;  // 1
	
	// [] 小标运算符
	int hoge_second = hoge[1];
	int hoge_third = *(p+2);
	cout << "\n" << hoge_second;
	cout << "\n" << hoge_third;
	
	// ->运算符
	hoge_struct* hogeStru_p = &hogeStru;
	cout<< "\n"<< hogeStru_p->hoge;
	cout<< "\n"<< (*hogeStru_p).hoge;
}

void (*func_p)();
void func()
{
	cout << "func enter";
}

// 函数指针引起的混乱
void test_pointer_08(){
	func_p = func;
	// 函数调用运算符() 的操作数不是 “函数”,而是“函数的指针”
	func_p();
}

// 
void test_pointer_09(){
	char str[10];
	//  error: incompatible types in assignment of 'const char [4]' to 'char [10]'
	// str = "abc";
	strcpy(str,"abc");
	cout<< "str: " <<str;  // abc 
	printf("\nstr: %p: ",str); // 000000000061fde6
	printf("\nstr: %s ",str); // abc
}

// ------------函数指针 start---------------
int (*func_01)(int a,int b);
int sum(int a, int b){
	return a+b;
}

int multiply(int a, int b){
	return a*b;
}

void test_function_pointer()
{
	// 将sum函数地址赋值给 函数指针
	func_01 = sum; 
	printf("sum: %d",func_01(1,2));  // 输出3
	
	// 同时也可以将 multiply 函数地址赋值给 函数指针
	func_01 = multiply;
	printf("\nmultiply: %d",func_01(1,2)); // 输出 2
}
// -------------函数指针 end--------------------------

// -------------函数指针充当形参 start, 冒泡排序------------------------------

// 升序
bool descend(int a, int b)
{
	return a>b ? true:false;
}

// 降序
bool aescend(int a, int b)
{
	return a>b ? false:true;
}

void bubble_sort(int arr[],int size, bool (*complete)(int,int))
{
	int temp;
	for(int i =0; i<size-1; i++)
	{
		for(int j = i+1;j<size;j++)
		{
			if (complete(arr[i],arr[j]))
			{
				temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
	
}

void test_bubble(){
	int arr[] = {1,5,6,0,35,85,410};
	int arr_size = sizeof(arr)/sizeof(arr[0]);
	bubble_sort(arr,arr_size,aescend);
	for(int i = 0; i< arr_size;i++)
	{
		printf("%d:\n",arr[i]);
	}
}
// -------------函数指针充当形参 end, 冒泡排序------------------------------

int main()
{
	
	test_bubble();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值