关于指针
本章节,我们重点分析C指针,首先我们从指针的概念谈起
💚💚💚
- 指针是一种保存变量地址的变量,并在C中频繁使用
- 在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
我们用一个图来说明下。
- 指针变量 hoge_p保存了另外一个变量 hoge地址,我们认为 “hoge_p” 指向 “hoge”。
- 对 hoge变量实施 &运算符得到 “hoge地址” 。 有时也称:"hoge的地址"的为 “指向 hoge的指针” (实际上这里的指针指:指针类型的值)
- 在 指针前面加上 * 可以表示指针指向的变量。(hoge_p 指向hoge), 所以 , *hoge_p 等同于 hoge , 那么一旦要求输出 *hoge_p 就会输出 hoge保存的值 。
🧡🧡🧡 要点
- 对变量使用 & 运算符,可以取得该变量的地址。 这个地址称为指向 该变量的指针。
- 指针变量 hoge_p 保存了指向其他变量的地址情况下(如保存 hoge 地址)可以说 “hoge_p 指向 hoge” 。
- 对指针变量运算 *运算符,就等同于它指向的变量。(如 hoge_p指向 hoge, *hoge_p就等同于 hoge)。
2. 常见疑问:指针就是地址,那么int的指针和double的指针有什么区别 了
- 如果从指针变量的角度说,指向这两个类型的指针没有区别,都保持相同的表现形式,与指针类型无关。
💚(尽管C标准没有规定所有数据类型的长度,但通常是这样一种情况:整数类型指针长度是一样的, char 类型指针和 结构体指针 长度一致,函数指针长度与数据指针长度不同)
💚 指针长度取决于使用的机器和编译器,通常在 现代 windows上,指针是32 位或64位长,对于DOS来说是 16位长。 - 从指针运算的角度来说,就需要关注指针类型了
💚(如:对指针加 N 即 :指针前进 “当前指针指向的数据类型的长度 * N”) - 指向任何类型的指针类型 ------ 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. 常见疑问:指针运算
- C指针运算是其他语言没有的
- 指针运算是针对指针进行整数加减运算
// 指针的运算
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语言需要存在指针运算符这样奇怪的功能了 ?
- C继承了 早期的 B 语言影响
- 使用指针运算可以写出高效的程序。
▲●通过角标的方式访问数组,array[i] 在循环中会出现多次,每次都要进行相当于 *(array + i) 的加法运算,效率自然是比较低的
▲●但是通过 p+i 的方式,加法运算只有在循环结束的时候执行一次。
💚 如今,编译器在不断被优化,对于循环内部重复的表达式会集中处理,是编译器优化的基本内容,对于现在一般的C编译器,无论你使用的数组角标还是指针来访问 数组元素,效率上都不会出现明显的差距,基本删都是输出完整的机器码。
5. 试图将数组作为函数的参数进行传递。
从上几章节我们知道:
**💚 数组 buf[ ] 可以解读成 :
- 指向它的初始元素的指针
- *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. 什么是空指针
💚 空指针是一个特殊的值
- 空指针是指可以确保没有指向任何一个对象的指针,通常使用 宏定义 NULL 来表示空指针常量值
- 空指针确保它和任何非空指针进行比较都不会相等,因此经常作为函数发生异常时的返回值使用
- 在现今的操作系统下,应用程序一旦视图通过空指针引用对象,就会马上招致一个异常并且当前应用程序会被操作系统强制终止
💚 常量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. 什么是指向数组的指针
- “数组”和“指针”都是派生类型,他们都是由基本类型开始重复派生生成的。也就是说派生出“数组”后,再派生出“指针”,就可以生成“指向数组的指针”。
- 我们需要区分:指向数组的指针和 指针数组初始元素的指针
// 指向数组的指针
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;
}