C语言 指针
概念:指针是一个变量,它的值是一个地址,也就是说,它指向存储在计算机内存中的另一个变量的所在的内存位置。
指针的使用细节:
- 指针变量的名字,是不包含 * 的 ,比如
int* p = &a;
这些 p 才是指针变量名 - 指针变量的数据类型必须要和指向变量的数据类型一致。
- 指针变量占用内存的大小,跟数据类是不一样的,跟编译器有关。 32位的操作系统,占用 4 个字节 ,64位操作系统,占用 8 个字节
- 给指针赋值的时候,不能把一个数值赋值给指针变量。 (指针变量存储的已经分配好的内存地址值)
简单点说:指针就是一个地址值,他指向内存中另一个变量的位置。
使用;
// 指针
int a = 10;
int* p = &a;
printf("%p \n", p);
printf("%d \n", *p);
// 利用指针修改变量的中数据
*p = 200;
printf("%d \n", *p);
printf("%d \n", a);
注意点:
- . &+ 变量名的意思是获取该变量的内存地址值 【这里的内存地址值,指的首地址】
int* p
这里的 * 是一个标记,标记该变量存储的指针*p
这里的 * 代表根据指针获取真实的数据,这个也就是 解引用。- 指针的数据类型必须要和 变量的数据类型一致,【指针存储的变量的首地址值和数据类型 才能正确的访问的数据】
- 指针的大小和数据类型无关,和操作系统有关 32 位操作系统指针大小是 4字节 , 64 位操作系统指针大小是8个字节
- 定义指针的时候不能将数据直接赋值给指针,原因是:指针中存储一个已经存在的地址值
细节:
- 指针 = 内存地址值
- 指针变量 = 内存内存地址的变量
- 定义指针变量的时
*
一个标识符 - 对指针进行操作的使用的
*
是一个解引用运算符,获取指针指向的变量的数据。
基地址 (首地址)
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,期地址是其所有字节中编号最小的那个,称为基地址。
符号:
- &取地址符,如果后面跟的是变量,取地址符获取的是当前变量所在内存地址,如果他后面跟的是地址,代表扩大地址到上一级类型的范围 ( 比如数组 , &arr ,范围就代表整个数组 )
- *解引用符号,代表缩小一级范围.他跟&是正负关系可以互相抵消,当地址缩小到不能再缩小,就会返回地址里的数据
这个两个符号的作用相反的,如果同时使用就会相互抵消掉。
指针变量
指针变量就是一个专门用来存放内存地址的变量.语法:
类型 *变量名;
类型(针指变量指向的内存空间存放的值得类型) *(声明告诉程序该变量是个指针变量)变量名 = 地址;
int a = 12;
int *p = &a;
// 创建一个指针类型变量,
// 指针类型满足两个条件
// 1. 这个代码要让程序一眼看到变量名就知道他是指针,
// 2. 程序知道指针指向的值是什么数据类型
例子:
int a = 20;
int *p = &a;
printf("%p \n", &a); // 0xff00aa08
printf("%p \n", p); // 0xff00aa08
printf("%p \n", &p); // 0xff00aa00
printf("%d \n", *p); // 20 星号解引用符,作用先拿到 p 的值 0xffffcc04(地址),访问这段内存
// *p == (*(&a)) == a
上面的图片:表达的意思是 p 变量中八个字节是用来存储 a 变量的首地址值的。
指针字符串(只读字符串)
使用指针变量存储字符串时,本质上只是存储了字符串数组的首元素(首字符)地址.
字符串指针支持数组的中括号语法进行内存地址遍历访问.
char str[100] = "hello world";
char *p = str;// &str[0]
char *p1 = "hello world2";
printf("%s \n", str); // printf 打印字符串要的是地址
printf("%s \n", p); // 将首字母地址传入printf,输出自动向后寻址知道\0结束
printf("%s \n", p1);
// 打印字符
printf("%c \n", *p); // h
printf("%c \n", *(p1 + 6)); // w
printf("%c \n", p1[6]); // w
p[1] = 'c';
// *(p + 1) = 'c'; 等价于上者
printf("%s \n", p);
// *(p1 + 1) = 'c'; // p1是字符串常量,这段内存只读不可修改
// printf("%s \n", p1);
使用 指针 + 双引号声明的字符串是只读,存放在内存中的只读常量区中。
只读常量区特点:
- 存储的数据是不能修改的,如果强行修改就会报段错。
- 在内部存储的数据是可以复用的,(若需要声明一个只读数据,会先检查只读常量区中是否存在该数据如果在就复用返回对应的内存地址,如果不存在才会开辟新的内存空间)。
指针数组:
含义:存储指针的数组
注意:
- 数组中元素是一个一个指针
- 若需要获取指针指向的数据需要使用
*(arr[i])
#include <stdio.h>
int main(int argc, const char* argv[]) {
int a = 10, b = 20, c = 30;
int* arr[3] = { &a , &b , &c };
for ( int i = 0; i < 3; i++ ) {
printf("%d \n", *(arr[i]));
}
return 0;
}
数组指针
含义:数组的指针
#include <stdio.h>
int main(int argc, const char* argv[]) {
char str[14] = "www.baidu.com";
// 获取数组中的 u ,这里使用直接使用指数组指针了。
char (*p)[14] = &str; // 获取这个字符数组的地址值,
// 直接获取数组指针步骤
// 1. 去处变量名,只留下数据类型 char sr[14] ----> char [14]
// 2. 加上(*指针名) --- > char (*p)[14]
// 3. 赋值:char (*p)[14] = &str
printf("%c \n", (*p)[8]);
char* p1 = str; // 这里获取字符数组中首地址,
printf("%p \n", str);
printf("%p \n", p1);
printf("%c \n", p1[8]); // 通过首地址就可以使用 p1[i] 这种语法糖来访问数据了
// 指针运算:移动的字节数是由 指针代表的范围(数据类型对应的字节数) * 步长数
// 指针代表的范围等于数据类型 p 的数据类型是 char [14] , p1 代码的数据类型是 char
// 虽然 p 和 p1 记录的地址是一样的,但是表达的含义是不一样的
// p 代表的是整个数组地址,在进行地址运算的时候 p[1] ==> p + 1 ===> 14 * 1
// p1 代表的是数组的首地址,也就是第一个元素的地址 p1[1] ==> p + 1 ==> 4 * 1
printf("%p \n", p);
printf("%p \n", p1);
return 0;
}
注意:
- 一定分清楚 &str 和 str 区别
- &str 代表的整个数组的 地址
- str 代表的数组中首个元素的地址
- 移动的字节数 = 数据类型的字节数 * 步长数, &str 代表是整个数组 14 个字节 , str 代表首个元素 1 一个字节。
声明数组指针注意点:
- 指针的名字需要使用 (* 指针名) , 原因是 * 号的优先级比中括号低,如果不加中括号就成了指针数组了。
- 在获取数组中的地址时候一定要使用
&
符号来获取,如果不是使用使用&
获取地址包含的范围就只有第一个元素的范围,使用了了&
来取地址,指针的范围就是整个数组的范围
直接使用数组指针问题:直接使用数组指针指向一维数组使用起来没有,使用元素指针的指向首元素方便。
所以:我们一般都使用数组指针指向数组的内的元素。
- 将二维数组:转换为一维的数组指针来使用
#include <stdio.h> int main(int argc, const char* argv[]) { int arr[2][3] = { { 1,2,3}, {4, 5 ,6 } }; // 上面二维数组的元素 int [3] int (*p)[3] = arr; // 获取的数组中第一个元素的首地址。 printf("%d \n", p[0][1]); return 0; }
创建步骤:
- 获取数组元素类型 --->
int arr[2][3]----> int [3]
- 加上(指针名字和标识符 )
int [3] ---> int (*p)[3]
- 赋值
int (*p)[3] = arr
赋值赋的首地址值。
使用细节:
- 这是用的指针p 代表的二维数组中第一个元素(整个一维数组)的地址
- 可以使用 [] [] 来语法糖来操作数组中的数据。
- 公式:
p[i] == *(p + i) , p[0][0] == (*p +0 )[0] == (*p)[0]
区分指针数组还是数组指针的总结
-
在数组指针之间定义的时候,判断这个变量是指针还是数组,通过运算符的优先级: () > [] > *
- 如果变量首先与[]结合,那么这个变量就是一个数组
- 如果变量首先与*结合,那么这个变量一定是个指针
-
判断指针变量的类型,变量名遇到
*
号就要停下来,剩余的东西都是用来修饰这个*
(指针)是内容的类型的具体来说:
- 遇到
*
号: 在指针变量定义中,第一个出现的*
号表示这是一个指针变量 (离变量名最近那个 * 和变量的组合就是一个指针了。)。例如,int *p;
中的*
表示p
是一个指向int
类型数据的指针。 - 剩余部分:
*
后面的部分用来描述指针指向的数据类型。例如,在int *p;
中,int
表示p
指向的数据类型是int
。
- 遇到
-
注意:一定要判断是指针数组还是数组指针 ,如果是数组指针就使用上面的方式判断具体的数据类型,如果指针数组,就像操作数组一样操作就行了。
指针使用
利用指操作其他函数中变量的值
使用指针修改其他函数中变量的值原因:在函数调用处传递过去的数据是变量的值,就变量的值赋值形参,操作操作形参,不会影响实参的数据。
// 指针的第一个作用:操作其他函数中变量的数据
// 这里是通过地址修改数据,地址值对应的数据发生改变了。
void swap2(int* p1, int* p2) {
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void getMaxAndMin(int arr[], int len, int* max, int* min) {
for (int i = 1; i < len; i++) {
*max = *max > arr[i] ? *max : arr[i];
*min = *min < arr[i] ? *min : arr[i];
}
}
// 函数的返回值作为函数执行的状态,计算结果利用指针返回
int getRemainder(int num1, int num2, int* res) {
if (num2 == 0) {
return 1;
}
*res = num1 % num2;
return 0;
}
int main() {
int a = 10;
int b = 20;
printf("交换前:%d , %d \n", a, b);
swap2(&a, &b);
printf("交换后:%d , %d \n", a, b);
// 示例二:
// 利用通过指针修改其他函数中数据,来实现定义函数,求出数组的最大值和最小值
int arr[] = { 0,3,45,3242,6,2,7 };
int len = sizeof(arr) / sizeof(arr[0]);
int max = arr[0];
int min = arr[0];
getMaxAndMin(arr, len, &max, &min);
printf("最大值:%d , 最小值:%d \n", max, min);
// 示例三:
// 通过指针可以修改其他的函数的变量,来将函数的执行状态和函数的执行结果分离开
int a = 10;
int b = 3;
int res = 0;
int flag = getRemainder(a, b, &res);
if (!flag) {
printf("函数执行成功余数为;%d \n", res);
}
else
{
printf("函数执行异常");
}
return 0;
}
注意点:
- 调用处传递的数据要是地址值就是
& + 变量名
- 形参要写指针
int* 变量名
- 修改时写
*变量名 = 值
- 利用指针将函数的执行结果放入之中来当做函数的返回值。
- 指针变量保存的地址值就是,被指向变量的内存地址
指针计算
- 指针计算:指针地址偏移的数量 = 指针指向的数据类型占用的字节数 * 加上的数值(减去的数值);(注意一定是指针指向的数据类型的字节数)
- 两个指针相减: 返回值是两个指针地址之间间隔多少个该指针指向类型的数据
double arr[] = {1, 2, 3, 4, 5}; // 6 7 8
double *p1 = arr;
double *p2 = &arr[7];
printf("%d", p2 - p1); // 7 这段内存间隔多少double
int *p3 = arr;
int *p4 = &arr[7];
printf("%d", p2 - p1); // 14 这段内存间隔多少int
#include <stdio.h>
int main() {
// 指针计算
/*int a = 10;
int* p = &a;
printf("%p \n", p);
printf("%p \n", p + 1);
printf("%p \n", p + 2);
printf("%d \n", *(p + 2));*/
// 如果想要指针的运算有意义,就必须是一个操作一个连续空间,数组中的元素的指针就是连续不间断的
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
printf("%p\n ", p);
// 通过指针获取 arr[1] 的值
printf("%d\n", *(p + 1)); // 2
// 计算数组中两个元素中间隔了多少个元素 , 连续空间中指针相减是有意义的
int* p2 = &arr[9];
printf("%d \n", (p2 - p)); // 9
return 0;
}
指针计算有意义的操作:【这里的有意义也是要在 , 指针指向是连续的空间,才有意义】
- 指针和整数向加减操作 (每次移动一个步长)
- 指针与指针进行相减操作 (计算间隔步长), 比如指针指向的一个数组时,如果要计算两个索引直接的间隔,就可以使用指针 - 指针来得到。
- 符号优先级:
( ) > [ ] > * > +、-
; 比如:*p + 1 ===
无意义的操作: 【 不知道最总的目标是什么 】
- 指针和整数进行乘除法
- 指针之间进行 加、 乘、除法
野指针
指向一段未知区域的指针,野指针是很危险的。
危害:
1. 引用野指针,相当于访问了非法内存,常常会导致数据段错误(segmentation fault);
2. 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
产生原因:
- 定义指针,但未初始化;
int *p;
// 后续没有赋值; *p就是个野指针
printf("%d", *p); // 这样可能发生段错误,也可能引用其他内存空间的数据
2. 指针所指向的内存,被系统回收
3. 指针越界
如何防止:
- 指针定义时,及时初始化
- 绝不引用已被系统回收的内存
- 确认所申请的内存边界,谨防越界
悬空指针
指向已经被释放的空间
比如函数中返回的地址,在调用处使用
#include <stdio.h>
int* add(int a, int b) {
int c = a + b;
int* p = &c;
return p;
}
int main(int argc, const char* argv[]) {
int* res = add(10, 20);
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("拖点时间\n");
printf("%d \n", *res);
return 0;
}
当使用使用 函数返回的指针进行解引用就会出现一错误的数据。
解决:不要使用指向已经被释放的内存空间的指针。
空指针
很多情况下,我们不可避免的会遇到野指针,比如刚定义的指针无法立即为其分配内存空间,又或者指针所指向的内存被释放了等情况。一般的做法就是给这些危险的野指针重新指向一段安全确定的内存,如零地址内存。
NULL 零地址该地址任何程序进行操作
从物理层面看,大部分机器的物理地址都是从0开始,在操作系统中为了安全和兼容性考虑,从0地址开始的一部分内存空间通常被操作系统占用,用于存放启动代码、中断向量等关键数据,这段内存是程序无法访问的。
概念:
- 空指针即保存了零地址的指针,亦即指向零地址的指针。
- 空指针可以使用宏定义 NULL 或字面量 0
// NULL 是一个宏定义,在Ubuntu /usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int *p2 = NULL;
// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3); // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL; // c. 让 p3 指向零地址
用途:
- 零地址(空指针)在编程中常用来表示指针的无效性,即指针不指向任何内存空间,通过空指针可以检测野指针问题,提高代码的健壮性。
- 直接访问0地址是非法的,因为操作系统会保护这部分内存空间,直接访问会产生(segmentation fault)或者空指针异常.
int *p3 = NULL;
printf("%d", *p3); // 发生段错误,也可能引用其他内存空间的数据
void* 通用类型指针
通用类型指针: 该指针可以指向任何类型,但是在解引用时,必须强制类型转换为指定种类的类型才可以解引用.
int a = 12;
void *p = &a;
printf("%d \n", *(int *)p);
总结:
- 在定义指针变量后,一定要赋值NULL安全值,防止出现"野指针"
- 只有void *p 没有 void p;
- 解引用 void *p 通用指针时,必须在解引用之前将指针进行强制类型转换为其他类型
- void 可以存储任地址【任何数据类型的指针都可以放入 void 指针中】
- void 指针获取不了值的,并且不能进行指针运算 【指针获取数据的前提: 知道首地址 和 步长 (void 类型没有步长)】
const修饰指针
例一: 当 const 修饰指针类型时,指针指向内存空间内的数据不可改变
const 修饰指针指向的字符 char, 一但尝试修改指针的char编译器就会报错
char str[] = "hello";
const char *pstr = str;
pstr[1] = 'H'; // 报错
将 const 移动到类型的右边与上面的代码等价
char str[] = "hello";
char const *pstr = str;
pstr[1] = 'H'; // 报错
但是将 const 移动到声明指针的*号右边,意义就完全不同了
因为此时const修饰的是指针自身, 表达的意思是该指针不能再指向别的地址值了。
char str[] = "hello";
// const 直接修饰指针变量,这个指针就不可以指向别的内存地址了。
char * const pstr = str;
pstr[1] = 'E'; // 正确
char str1[] = "world";
pstr = str1; // 错误: 此时 pstr 指针不可以再指向其他地址
pstr = NULL; // 错误: 此时 pstr 指针不可以再指向其他地址
总结:
-
const 修饰指针 , 不可能通过指针来修改指针指向的内存空间的数据, 但是可以通过让指针指向一块新的内存空间
char str[] = "hello"; const char *pstr = str; pstr[1] = 'H'; // 报错 char s = "world"; pstr = s ;
-
const 修饰指针变量 , 可以通过指针来修改内存空间的值,但是不能让指针指向一片新的内存空间
char str[] = "hello"; // const 直接修饰指针变量,这个指针就不可以指向别的内存地址了。 char * const pstr = str;
-
修改内存中数据有两种方式,
- 直接修改变量的值
- 通过指针修改变量的值。
二级指针和多级指针
概念:指向一级指针的指针。
格式:
#include <stdio.h>
int main() {
// 二级指针
int a = 10;
int b = 20;
int* pa = &a;
int* pb = &b;
printf("pa =%p \n", pa);
printf("pb = %p \n", pb);
int** ppa = &pa;
// 修改存储的一级指针地址
//ppa = &pb;
printf("ppa = %p \n", *ppa);
// 修改一级指针存储的地址值: 将 *ppa 存储的一级指针 pa 对应的地址,修改成 pb 对应的地址值
*ppa = pb;
printf("pa =%p \n", pa);
printf("pb = %p \n", pb);
// 获取一级指针中存储的
printf("%d \n", **ppa);
return 0;
}
示例:
#include <stdio.h>
#include <stdlib.h>
void test(char** pp) {
char* str = "www";
// 通过改变指针中指向的地址,来给修改指针
*pp = str;
}
int main()
{
char* ptr;
test(&ptr);
printf("%s \n", ptr);
return 0;
}
总结:
- 看星号的个数来确定指针的层级
*
根据地址获取地址对应的数据- 一级指针的数据就是是
数据类型*
数组指针
所谓的数组指针其实就是指向数组的指针。
使用
获取数组中元素
#include <stdio.h>
int main() {
int arr[] = { 1,2,3,4,5 };
int len = sizeof(arr) / sizeof(arr[0]);
printf("%p \n", arr);
int* p = arr;
printf("%p \n", p);
for(int i = 0 ; i < len ; i++){
printf("%d \n", *(p + i));
}
return 0;
}
}
注意点
- 数组指针在参与运算的时候,会退化成第一个元素的指针 【也就是数组的首地址值】
- 做成参数传递是,也是会退化的。
- 有两个特殊情况不会退化
- 使用
sizeof
运算不会退化 - 使用
&数组变量
获取地址不会退化 ,但是这里的获取的到的地址也就是指针在运算的时候,步长就是整个数组的字节数了 【sizeof
运算出来的值】- ( + 1 就会跨过整个数组 )
#include <stdio.h>
int main() {
int arr[] = { 1,2,3,4,5 };
// sizeof 运算不会退化
int len = sizeof(arr) / sizeof(arr[0]);
// 都是获取数组的首地址 , 但是在参与运算不一样
printf("%p \n", arr );
printf("%p \n", &arr );
printf("------------- \n" );
printf("%p \n", arr+1 ); // 步长是 4 字节
printf("%p \n", &arr+1 ); // 步长是 20 个字节
return 0;
}
小结巧:只要引用数据类型,变量的值的就是地址值,就代表着可以直接赋值给指针。如果是基础数据类型就需要通过 &
获取到变量的对应的地址值,才能赋值给指针。引用数据类型的指针类型和引用数据中包含的基础数据一直。
二维数组在开发中常用写法
语法:
配合指针实现的二维数组在开发中非常常见,习惯这种写法 ( 利用指针将二维数组降纬成一维数组来使用 )
int (*p)[3] = {{1, 2, 3}, {4, 5, 6}};
printf("buf[0][0] = %d \n", p[0][0]);
以下值得演变过程
int buf[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 类型 int [3]
// 定义了一个指针p,存储二维数组的名字
int(*p)[3] = buf; // 指针指向 类型 int [3]
printf("buf[0][0] = %d \n", **buf); // *&buf[0] -> buf[0] -> &subbuf[0]
printf("buf[0][0] = %d \n", *(buf[0] + 0));
printf("buf[0][0] = %d \n", buf[0][0]);
printf("buf[0][0] = %d \n", **p); // *&buf[0] -> buf[0] -> &subbuf[0]
printf("buf[0][0] = %d \n", *(p[0] + 0));
printf("buf[0][0] = %d \n", p[0][0]);
总结:
-
在数组指针之间定义的时候,判断这个变量是指针还是数组,通过运算符的优先级: () > [] > *
- 如果变量首先与[]结合,那么这个变量就是一个数组
- 如果变量首先与*结合,那么这个变量一定是个指针
-
判断指针变量的类型,变量名遇到
*
号就要停下来,剩余的东西都是用来修饰这个*
(指针)是内容的类型的
char *(*p)[3]; // p是一个指针, 他的类型 char * [3] ,类型 char [3] 指针
//p是一个指针指向一个 char [3] 数组类型的数组指针
char **p[3] // p是一个数组, 数组中包含三个 char ** 类型指针指向 char *
函数指针
函数指针的作用:动态的调用函数
函数指针定义:
#include <stdio.h>
void fn1() {
printf("fn1 执行了 \n");
}
int fn2(int a, int b) {
printf("fn2 执行了 \n");
return a + b;
}
int main() {
// 函数指针
void (*p1)() = fn1;
int (*p2)(int, int) = fn2;
// 通过指针调用方法
p1();
int res = p2(2, 4);
printf("res : %d \n", res);
return 0;
}
定义指针的书写方法:
- 复制函数的声明
- 修改声明将声明中的函数名替换成 (* 指针名)
- 赋值成函数名【注意这里不是调用函数,不要加上 括号】
函数指针数组
#include <stdio.h>
int fn1(int num1, int num2) {
return num1 + num2;
}
int fn2(int num1, int num2) {
return num1 - num2;
}
int fn3(int num1, int num2) {
return num1 * num2;
}
int fn4(int num1, int num2) {
return num1 / num2;
}
int main() {
int a, b;
printf("输入两个数值:");
scanf("%d %d", &a, &b);
// 定义函数指针数组 ,格式在 定义函数的基础在指针变量中加上 [函数个数]
int (*arr[4])(int num1, int num2) = { fn1,fn2 , fn3,fn4 };
int c;
printf("输入操作:\n");
scanf("%d", &c);
// 通过指针调用函数,arr[0] 代表获取第一个
int res = arr[c - 1](a, b);
printf("%d ", res);
return 0;
}
注意点:
- 函数指针内的函数必须要是参数一样并且返回值一样的函数,才能返回同一个函数指针数组中.
- 书写方式 在原来指针函数声明中变量中加上 [函数个数] 示例:
int (*arr[5])(int ,int ) = {fn1,fn2,fn3,fn4}