C/C++中指针
指针在C或C++语言中都是一个非常重要的概念,可以不夸张的说,掌握了指针就相当于掌握了C语言的核心。指针是一个变量,它的值为另一个变量的地址。使用指针可以非常灵活的访问或修改它指向的变量。
1、指针的定义与访问
指针定义方式如下:
数据类型 * 变量名
数据类型是指针指向变量的类型,*是一个固定的标识,标记这是一个指针变量,如:
int* p1; // 定义一个指向int类型的指针
double* p2; // 定义一个指向double类型的指针
struct Color* p3; // 定义一个指向结构体的指针
上面定义了几种类型的指针,要对指针进行赋值可以在定义时直接初始化,也可以先定义后赋值,对指针变量赋值必须是某个变量的内存地址:
int num = 10;
int* p1 = # // 定义指针时直接初始化
int* p2;
p2 = # // 先定义指针后赋值
对于不指向任何地址的指针,可以在定义时赋值NULL;要获取指针变量中存储的地址,可以去掉定义时的*号即可;要访问指针变量指向地址中存储的数据,可以通过*号获取。
int num = 10;
int* p = #
printf("%p\n", p); // 打印指针指向的地址
printf("%d\n", *p); // 打印指针指向地址中的值
在这个示例中注意指针定义和取值时都使用到了*号,但要区别两个*号的不同:在指针定义时*表示定义的变量是一个指针变量,与普通的变量不同,指针变量只存放指向变量的地址;而在取值时的*号表示获取指针变量指向地址中的值。所以为了区别两个*号的不同,在定义指针变量时,将*与数据类型紧贴在一起,取值时*与指针变量紧贴在一起。
2、空指针、野指针和悬空指针
空指针 是指:指针变量指向内存中编号为0的空间,这部分内存空间是操作系统占有,用户程序是不允许访问的。空指针的作用是定义指针变量时初始化。
int* p = NULL; // 未指向任何地址的指针
printf("%p\n", p); // 打印指针地址
野指针 是指:定义了指针变量但是指向的地址空间是非法的。
int* p1;
int num = 10;
p1 = # // 已经分配指向的地址空间printf("%p\n", p1);
printf("%d\n", *p1);int* p2 = p1 + 4; // 指向的地址空间是非法的printf("%p\n", p2);
printf("%d\n", *p2); // 这里访问p2指向地址空间并未分配,它的值是不确定的
悬空指针 是指:分配了指向的地址空间,但是地址空间被释放了。
#include <stdio.h>
#include <stdlib.h>int* sum(int a, int b);int main() {// 这里函数执行完后对应的栈栈空间会被释放,所以函数里面定义的指针也会释放int* p = sum(10, 20);printf("等待\n");printf("等待\n");printf("等待\n");printf("等待\n");printf("--------\n");printf("%d\n", *p);return 0;
}int* sum(int a, int b) {int sum = a + b;int* p = ∑return p;
}
3、指针运算
指针可以运算的,比如 ++、–、+、- 等运算符,它表示指针向前或向后移动的步数,而具体移动多少字节,是根据指针的数据类型决定的,比如下面的代码:
#include <stdio.h>
#include <stdlib.h>int main() {int num = 10;int* p1 = #printf("%p\n", p1); // 输出指针指向的内存地址printf("%p\n", (p1 + 1)); // 地址加1,指针移动4字节,因为int类型是4字节printf("%p\n", (p1 - 1)); // 地址减1printf("----------------\n");short s = 10;short* p2 = &s;printf("%p\n", p2);printf("%p\n", (p2 + 1)); // 地址加1,指针移动2字节,因为short类型是2字节printf("%p\n", (p2 - 1));return 0;
}
代码执行后的输出如下:
006FFEAC
006FFEB0
006FFEA8
----------------
006FFE94
006FFE96
006FFE92
通过上面的输出可以知道,指针计算后具体移动多少字节是跟指针的数据类型有关。
4、指针占用内存空间大小
指针占用多少内存空间与数据类型无关,它只与操作系统有关,32位的操作系统,用4字节表示内存地址,所以指针就占用4字节大小;64位系统,用8字节表示内存地址,所以指针就占用8字节。
#include <stdio.h>
#include <stdlib.h>int main() {int num = 10;int* p1 = #short s = 10;short* p2 = &s;printf("%d\n", sizeof(p1));printf("%d\n", sizeof(p2));return 0;
}
在32位编译器和64位编译器下它们分别输出4和8。可见指针占用内存空间大小只与系统有关,与指针指向的地址存储的数据类型无关,也说明了指针存储的就是它指向的内存地址。
5、void类型指针
c语言是强类型语言,不同类型变量一般是不能相互赋值的,指针也是一样遵循这个规则。但是void类型指针确没有这个限制,可以将任意类型的指针赋值给void类型指针,也就是说void类型指针可以指向任何类型的数据而无需强制类型转换,所以void类型指针可以称为通用类型指针:
#include <stdio.h>
#include <stdlib.h>int main() {int num = 10;int* p1 = #short s = 10;short* p2 = &s;printf("%d\n", sizeof(p1));printf("%d\n", sizeof(p2));void* p = p1;printf("%d\n", sizeof(p));p = p2;printf("%d\n", sizeof(p));return 0;
}
void类型指针是不能直接解引用访问所指向内存空间的数据,也不能对void类型指针进行算术运算,必须先将void类型指针强转为其他具体类型的指针后才能访问或操作。
#include <stdio.h>
#include <stdlib.h>int main() {int num = 10;int* p1 = #void* p = p1;printf("%d\n", *(int*) p); // 必须强转具体类型后才能访问return 0;
}
void类型指针的存在是有意义的,可以用void类型指针作为形参,也可以用void类型指针作为函数的返回值,这样写的函数会比较通用,在真正使用指针的地方做强制类型转换即可。
这里让我想起了java语言中的Object,它是所有类的父类,可以表示任何类,在具体是用时做强制类型转换就可以得到真正的数据类型。
下面举例void类型指针的使用:
如果我们要实现两个数值交换,如果不使用void类型指针,针对不同类型的字段要分别写多个交换数值的函数,而使用void类型指针就相对方便一些:
#include <stdio.h>
#include <stdlib.h>void swap(void* p1, void* p2, size_t num);int main() {int a = 10, b = 20;printf("before int : a = %d, b = %d\n", a, b);swap(&a, &b, sizeof(int));printf("after int : a = %d, b = %d\n", a, b);short c = 30, d = 40;printf("before short : c = %d, d = %d\n", c, d);swap(&c, &d, sizeof(short));printf("after short : c = %d, d = %d\n", c, d);return 0;
}void swap(void* p1, void* p2, size_t size) {rsize_t i = 0;for (; i < size; i++) {char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
6、二级指针和多级指针
二级指针 是指向指针的指针,指针指向的地址中存储的是另外一个指针的地址,定义方式:
数据类型 ** 指针变量
二级指针长用于修改一级指针里面记录的内存地址。
7、指针和数组
数组名指向的是数组首地址,本质上也是一个内存地址,与指针不同,用 sizeof() 计算数组占用内存大小时,它计算的是数组所有元素占用内存的字节总和。一般可以认为数组名就是指向数组首地址的指针,但它是一个常量指针。它不能像指针那样再次赋值指向另外的地址,也不能像指针那样进行加减运算。
数组名不能移动,但是可以定义一个指针指向数组,通过移动指针访问或修改数组元素的值,由于数组名指向的就是首元素的地址,所以指针指向某个数组有下面两种方式:p = &arr[0] 或 p = arr,这两种方式都可以将指针指向数组地址。
指针访问或修改数组元素的示例:
#include <stdio.h>
#include <stdlib.h>int main() {// 定义数组int arr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };// 定义指针指向数组int* p = arr;// 遍历数组打印数组的每一个值int len = sizeof(arr) / sizeof(int);for (int i = 0; i < len; i++){printf("%d\n", *p);p++;}// 修改数组的值p = arr;*(p + 3) = 33;printf("----------------\n");for (int i = 0; i < len; i++){printf("%d\n", *p);p++;}return 0;
}
把数组作为参数传递的时候,数组将退化为指针,数组在作为参数传递时,通常都是把数组首元素的地址作为参数来传递,这时候数组名就相当于数组首元素的地址,也就是指针。所以在函数体内,数组参数就失去了本身的内涵,可以像指针那样进行操作,就是说 void arr_size(int arr[]); 和 void arr_size(int* arr); 表达的含义是一样的:
#include <stdio.h>
#include <stdlib.h>void arr_size(int arr[]);int main() {// 定义一个数组int arr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };// 输出占用内存大小printf("主函数内数组占用内存:%d\n", sizeof(arr)); // 计算数组占用内存空间arr_size(arr);return 0;
}void arr_size(int arr[]) {printf("数组作为参数占用内存:%d\n", sizeof(arr)); // 退化为指针printf("%d\n", *(++arr)); // 可以像指针那样操作
}
数组名作为常量指针,可以通过一次寻址就可以找到数组在内存中的地址;而指向数组的指针需要两次寻址才能找到数组在内存中的地址(首先找到指针的地址,在通过指针指向的地址查找数组的内存地址)
#include <stdio.h>
#include <stdlib.h>int main() {// 定义一个数组int arr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };printf("%p\n", arr);printf("%p\n", &arr);int* p = arr;printf("----------------\n");printf("%p\n", p);printf("%p\n", &p);return 0;
}
指针对比数组的一个优势: 可以扩容, 因为指针的内存可以通过malloc分配, 所以也可以通过realloc扩容。
数组变量名表示这个数组的首地址,而 & 变量名也表示数组的首地址,但是这两个在执行指针运算时跳的步长是不同,第一种方式跳一个元素的长度;第二种方式跳一个数组的长度。
#include <stdio.h>
#include <stdlib.h>int main() {// 定义一个数组int arr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };printf("%p\n", arr);printf("%p\n", arr + 1);printf("----------------\n");printf("%p\n", &arr);printf("%p\n", &arr + 1);return 0;
}
# 第一种方式指针移动步长
0135F890 - 0135F88C = 4
----------------
# 第二种方式指针移动步长
0135F8B4 - 0135F88C = 40
8、指针和函数
函数的形参有两种传参方式:值传递和地址传递,值传递方式会在函数的栈空间重新开辟一段内存空间存储数据,所有对形参的修改都不会影响原来的实参;地址传递则不同,它是把实参的地址传递到函数内,实参和形参公用一段内存空间,所有对形参的修改同样也是对实参的修改。使用指针作为函数的参数,实际就是地址传递:
#include <stdio.h>
#include <stdlib.h>void swap1(int a, int b);
void swap2(int* a, int* b);int main() {int a = 10;int b = 20;// 值传递方式交换两个数printf("before a = %d, b = %d\n", a, b);swap1(a, b);printf("after a = %d, b = %d\n", a, b);// 引用传递方式交换两个数printf("----------------\n");printf("before a = %d, b = %d\n", a, b);swap2(&a, &b);printf("after a = %d, b = %d\n", a, b);return 0;
}void swap1(int a, int b) {int tmp = a;a = b;b = tmp;
}void swap2(int* a, int* b) {int tmp = *a;*a = *b;*b = tmp;
}
有些函数如果想返回多个值给调用方,在没有学习结构体的时候实现会比较难,这时候可以在函数的形参中传递多几个指针变量用于存储这些返回值。这就像别人要送给你一些礼物,但是你又没法直接接收,这时候你就可以给他一个空的箱子,告诉他把礼物放进箱子里,你自己用的时候去箱子里拿就可以了。
#include <stdio.h>
#include <stdlib.h>void get_max_min(int arr[], size_t size, int* max, int* min);int main() {// 定义一个数组int arr[10] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };int max = 0, min = 0;get_max_min(arr, sizeof(arr) / sizeof(int), &max, &min);printf("max = %d, min = %d\n", max, min);return 0;
}void get_max_min(int arr[], size_t size, int* max, int* min) {*max = arr[0];*min = arr[0];for (int i = 0; i < size; i++) {if (arr[i] > *max) {*max = arr[i];}if (arr[i] < *min) {*min = arr[i];}}
}
可以用指针作为函数的形式参数,通过函数指针,将回调函数传递给这个函数,在适当的时候执行回调函数,完成相关的操作。
#include <stdio.h>
#include <stdlib.h>int asc(int a, int b);
int desc(int a, int b);
void sort(int arr[], size_t size, int(*compare)(int, int));int main() {// 定义两个数组int arr1[10] = { 40, 10, 30, 20, 90, 0, 70, 80, 60, 50 };int arr2[10] = { 40, 10, 30, 20, 90, 0, 70, 80, 60, 50 };int size = sizeof(arr1) / sizeof(int);// 实现升序printf("asc sort: ");sort(arr1, size, asc);for (int i = 0; i < size; i++) {printf("%d ", arr1[i]);}printf("\n--------------------------------\n");// 实现降序printf("desc sort: ");sort(arr2, size, desc);for (int i = 0; i < size; i++) {printf("%d ", arr2[i]);}printf("\n");return 0;
}int asc(int a, int b) {if (a < b) {return -1;}else if (a > b) {return 1;}else {return 0;}
}int desc(int a, int b) {if (a < b) {return 1;}else if (a > b) {return -1;}else {return 0;}
}void sort(int arr[], size_t size, int(*compare)(int, int)) {int i, j, tmp;for (i = 0; i < size - 1; i++) {for (j = 0; j < size - i - 1; j++) {if (compare(arr[j], arr[j + 1]) > 0) {tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}
使用函数指针变量,根据不同的输入条件调用不同的函数,这样代码编写会更加灵活:
#include <stdio.h>
#include <stdlib.h>int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);int main() {// 定义一个函数指针:不用硬记格式,将方法名替换为【(* 指针名)】,删除所有的形参名int (* p)(int, int) = NULL;// 根据不同条件赋值不同的方法printf("enter operator[+、-、*、/] : ");char c;scanf_s("%c", &c);switch (c) {case '+':p = add;break;case '-':p = subtract;break;case '*':p = multiply;break;case '/':p = divide;break;}int a;int b;printf("enter int number1 : ");scanf_s("%d", &a);printf("enter int number2 : ");scanf_s("%d", &b);int rs = p(a, b);printf("result = %d\n", rs);return 0;
}int add(int a, int b) {return a + b;
}
int subtract(int a, int b) {return a - b;
}
int multiply(int a, int b) {return a * b;
}
int divide(int a, int b) {return b != 0 ? a / b : 0;
}
函数指针使用要注意,在调用之前必须要进行初始化,使其指向一个有效的函数;函数指针的类型必须与其指向的函数类型完全匹配,包括返回类型和函数的参数列表,否则编译不通过。