C语言——指针&数组
指针
指针是一种编程概念,它是一个变量或数据类型,用于存储内存地址。指针允许程序直接访问和操作内存中的数据,而不仅仅是访问变量的值。指针通常在低级编程语言(如C和C++)中使用,以及某些高级编程语言中的底层编程任务中。
关键要点关于指针包括:
- 内存地址:指针存储一个内存地址,该地址指向计算机内存中的某个位置。
- 指向:指针可以指向内存中的数据,这可以是基本数据类型(如整数、字符)或复杂的数据结构(如数组、结构体)。
- 解引用:通过解引用指针,可以访问指针所指向的内存地址上的值。解引用操作使用
*
符号。- 地址运算:指针可以进行地址运算,例如指针加法或减法,以访问相邻内存位置。
- 动态内存分配:指针在动态内存分配中非常有用,允许程序在运行时分配和释放内存,避免静态内存分配的限制。
- 传递参数:指针允许将变量的地址传递给函数,以便在函数内部修改变量的值。
好处
- 指针可以动态分配内存
- 在链表中可以方便修改链表的节点
- 解析字符串
- 相同类型的指针可以直接复制
调用Free释放内存后,指针还能用吗
Free释放掉内存后,只是把内存的使用权就被归还给系统,内存里面的东西可能被清除也可能是垃圾值,但是指向这个内存的指针还是指向这块内存,并不会NULL
指针不能加指针
指针之间可以做减法,但不能做加法
空指针是指指向地址为0的地方
数组
初始化
1 2 3 4 5
int a[][2]; //不允许 int b[][2]={1,2,3,4}; //可以 int c[] = {1,2,3}; // 可以 int c[]; //不可以 int d[][]; //不允许,第二个[]必须填,不管有没有初始化
数组名num / &num的区别
对于一维数组来说
num+1是偏移到下个元素,&num+1是偏移整个数组
对于二维数组来说
num+1是偏移一个一维数组,&num+1是整个数组
数组下标是负数
1 2 3 4 5 6 7 8 9 10
int a[5] = {0,1,2,3,4}; int *p = &a[4]; for(int i=-4; i<=0; i++) { printf(%d %d \n", p[i], &p[i]) } // 0 1310572 // 1 1310576 // 2 1310580 ……
二维数组
int a[3][3];
- int a[3][3];表示是个三行三列的二维数组
- 数组名表示数组首元素的地址,即第0行第0个地址
- a+1表示地址偏移一个一维数组的地址,即三列int大小=34 = 12
- *a 表示去二维变一维,*a就相当于一维数组的数组名,比如 *a +1 表示第0行下标为1的元素地址,只是偏移一个Int地址
- 若要表示a[2][2]的元素 即 *(*(a+2)+2)
指针的运算
1 2 3 4 5 6 7 8 9
// 例子1: int *ptr;//假设指针指向的地址是0x 00 00 00 00 Ptr++; //运算之后指针指向0x 00 00 00 04 Char *p; P++;//地址偏移1 // 注意:对于一级指针的++操作偏移量要看指针指向的是什么类型 // 对应二级指针偏移量,对于32系统是4个字节,因为二级指针指向的类型就是指针,所以永远是一个指针类型的大小
1 2 3 4 5 6 7 8 9 10 11
// 例子2: #include<stdio.h> int main() { char a[20]="You_are_a_girl"; char *p=a; char **ptr=&p; printf("**ptr=%c\n",**ptr); ptr++; printf("**ptr=%c\n",**ptr); } // 在这个例子中是无法确定二级指针++之后指向的地址内容,因为二级指针(ptr)指针指向的一级指针的地址,如果二级指针(ptr)++之后,那么就会指向一级指针的后4个字节(对于32位操作系统来说指针类型是4字节),至于这个地址里面是啥无从得知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// *p++ *(p++) (*p)++ *++p ++*p 的运算 int num[] = {1, 2, 3, 4}; 1. 后++先不用管,即先把地址里面的值给别人,再去++,可以是地址或值(看括号是否包住*p,是则是值++),后++有三种如下: (*p)++ 地址前后都不会变化,变化的是地址里面的值,先赋值给别人,*P再++ *(p++)和*p++一样 地址发生变化,先把*P赋值给别人,再++地址 2.前++,先++操作,可以是++地址或者值,(++符号靠近p就是地址++,靠近*P就是值++),再把值给别人 *++p 地址发生改变,先把地址++,再把地址变化后的里面的值给别人 ++*p 地址不发生变化,*P的值++之后再赋值给别人 注意:指针++,到底加几个字节要根据指针指向的类型决定,指针在32系统中永远是4个字节 举例子: Int * a;//假设指针指向的地址0 a++;//此时指针指向后移4个字节,即指向4 Char *b;//假设指针指向的地址0 b++;//此时指针指向后移1个字节,即指向1
1 2 3 4 5
// sizeof(数组名)和sizeof(&数组) int Num[100]; printf("%ld\n",sizeof(Num));//400 printf("%ld\n",sizeof(&Num));//8起始就是打印int *指针大小 printf("%ld\n",sizeof(int *));//8
指针数组
1 2 3 4 5
// 首先是个数组,这个数组里的元素是指针 int *p [4]; int a[4]={1,2,3,4}; p[0]=&a[0]; printf("%d\n",*p[0]);
1 2 3 4 5 6 7 8 9
// 首先是个指针,这个指针指向数组例如: Int (*p) [4];//表示一个指向有4个int 元素的数组的指针,所以p+1,加的是4个int int num[8] ={1,2,3,4,5,6,7,8}; int (*p)[4] ; p = num; printf("%d\n",sizeof(p));//8 因为p是指针,64位系统8个字节 printf("%p\n",p);//0x7ffe11d8a4e0 printf("%p\n",p+1);//0x7ffe11d8a4f0 //加的是指针指向的类型大小,这里指针指向的是有四个4int元素的数组,所以加的是16个字节
1 2 3 4 5
// 双指针 int b[12] = {1,2,3,4,5,6,7,8,9,10,11,12} int (*p)[4]; p = b; printf("%d\n", **(++p)); // 5
指针函数
1 2 3 4
// 函数的返回值是个指针类型 int * fun(int x,int y){ }
函数指针
1 2 3 4 5
int (*pf)(float); // 函数指针调用函数事注意以下几点: 1. 函数类型必须和函数指针的类型一样,比如参数类型,返回值 2. 给函数指针赋值是可以&也可以不要
指针和数组的区别
- 数据类型:
- 指针是一种数据类型,用于存储内存地址。指针可以指向不同数据类型的内存位置。
- 数组是一种数据结构,用于存储相同数据类型的一组连续内存单元。
- 大小:
- 指针的大小通常与系统架构相关,它存储一个内存地址,因此大小在不同系统上可能会有所不同。
- 数组的大小是由其包含的元素数量决定,每个元素的大小也是相同的。
- 初始化和赋值:
- 指针需要显式初始化为一个有效的内存地址,可以通过将其设置为某个变量的地址来初始化。
- 数组在声明时需要指定大小,而且在创建时会自动初始化,可以直接为数组元素赋值。
- 地址运算:
- 指针允许进行地址运算,例如指针加法或减法,以访问内存中的相邻位置。
- 数组的元素在内存中是连续存储的,因此可以通过索引来访问不同的元素。
- 传递给函数:
- 指针可以用于将变量的地址传递给函数,以便在函数内部修改变量的值。
- 数组在传递给函数时通常会退化为指针,因此函数接收到的是指向数组第一个元素的指针。
- 动态内存分配:
- 指针可用于动态内存分配,例如使用
malloc
或new
来分配内存,然后通过指针访问分配的内存。- 数组的大小通常在编译时确定,但C99标准引入了可变长度数组(VLA),允许在运行时指定数组大小。
指针和引用的区别
- 指针:
- 指针是一个变量,它存储另一个变量的内存地址。
- 指针可以为空(null),也可以重新分配给指向不同的变量。
- 指针需要使用解引用操作符
*
来访问所指向的值。- 指针可以进行指针算术,如指针加法或减法。
- 指针可以指向不同类型的对象,但需要进行强制类型转换。
- 指针可能需要显式地管理内存,包括分配和释放内存。
- 引用:
- 引用是一个别名,它为已经存在的变量提供了另一个名称。
- 引用在创建时必须与现有变量绑定,无法改变绑定到不同的变量。
- 引用的语法更简洁,不需要解引用操作符,直接使用引用就可以访问所绑定的值。
- 引用不支持指针算术。
- 引用通常用于传递参数给函数,以便在函数内部修改参数的值。
- 引用不需要显式管理内存,不涉及内存分配和释放。
引用区别于指针的特性是 :
- 不存在空引用(保证不操作空指针)
- 必须初始化(保证不是野指针)
- 一个引用永远指向他初始化的那个对象(保证指针值不变)
野指针
野指针:是指指针指向的地址是不确定的;
野指针(Dangling Pointer)通常是指指针变量存储了一个无效的内存地址,也就是它指向的内存区域可能已经被释放或不再有效。野指针的操作是不安全的,因为它们可能导致未定义的行为或程序崩溃。
野指针一般来说可以被重新赋值,但这并不会解决野指针的问题。重新赋值一个野指针只是改变了它的目标地址,但仍然可能会导致访问无效内存。在C和C++中,遵循以下最佳实践来处理野指针:
- 避免野指针:在使用指针前,确保它指向有效的内存区域。不要让指针指向已释放的内存或未分配的内存。
- 初始化指针:在声明指针时,始终将其初始化为
NULL
(C语言)或nullptr
(C++语言)。这可以帮助你检测是否有野指针。- 谨慎使用已释放的内存:如果确实要使用已释放的内存,确保在释放内存后不再访问它。
- 不要多次释放相同的内存:释放内存后,不要再次释放相同的内存块,否则会导致问题。
原因:
释放内存之后,指针没有及时置空
避免:
- 初始化置NULL
- 申请内存后判空
- 指针释放后置NULL
- 使用智能指针
1 2 3 4 5
int *p1 = NULL; //初始化置NULL p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 assert(p1 != NULL); //判空,防错设计 free(p1); p1 = NULL; //释放后置空
指针 & const
1 2 3 4
const int* p; //常量指针----->指针指向的地址的内容不可以改变 int const *p; //常量指针 int * const p; // 指针常量-------->指针指向的地址可以不改变 const int * const p; //指向常量的常量指针
指针减指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 地址相减 = (地址a -地址b)/sizeof(指针指向的类型) int a[3]; a[0] = 0; a[1] = 1; a[2] = 2; int *p, *q; p = a; q = &a[2]; printf("%p\n",p);//0x7ffe80e38b0c printf("%p\n",q);//0x7ffe80e38b14 printf("%d\n",q-p);//2 ( q - p) /sizeof(int ) // 那么就有 a[q-p] = a[2] = 2
指针作为函数参数传递问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
//函数1,交换两个数 void swap1(int *p,int *q) { int num = *p; *p =*q; *q = num; } //函数2,让p指针指向a,想重新给p赋值为90,之后再交换p,q void swap(int *p,int *q) { int a =90; p = &a; int num = *p; *p =*q; *q = num; } int main() { int a =2,b =3; int * j = &a; // 指针j指向a int * k =&b; //指针 k 指向 b swap(j,k); //调用函数 printf("a =%d b= %d\n",a,b);// a = 2 b = 90; return 0; } // 于函数swap1我们可以正常交换两个值,但是swap函数却不是我们想要的,我们想要的答案是 a = 3 b = 90;可是发现,a的值压根没变,这是为什么呢? // 在swap函数中,你将指针p指向了一个局部变量a,而这个局部变量在swap函数执行完毕后将被销毁。这会导致指针p指向一个不再有效的内存位置 总结: 如果在函数形参里的指针变量不修改指向,那么就会影响传递过来的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
void fun(char *s) { char a[10]; strcpy(a, "STRING"); s = a;//修改了形参指针指向,就不会影响传递过来指针 } void main() { char *p = "PROGRAM"; fun(p); printf("%s\n", p); //PROGRAM } // 拓展:通过二级指针操作 void fun(char **s) { char a[10]; strcpy(a, "STRING"); *s = a; // *s = "string"; } int main() { char *p = "PROGRAM"; fun(&p); printf("%s\n", p); //打印空白 return 0; } // 在这里我们通过把一级指针的地址传递给函数,函数二级形参来接受,那么*s表示的就是指针p指向的地址,可以看出这里直接就操作p的指针指向,所以如果 *s = “string”那么指针p就会指向”string”的地址
二维数组和数组指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
int main() { /*二维数组和数组指针*/ int (*p)[3]; int s[3][3]={1,4,7,4,9,6,2,7,9}; p = s; printf("p addr is = %p\n",p);//0x7ffde8cdf0a0 printf("s addr is = %p\n",s);//0x7ffde8cdf0a0 /*可以发现p+1和s+1是一样的都是加一个一维数组*/ printf("p + 1 addr is = %p\n",p + 1);//0x7ffde8cdf0ac printf("s + 1 addr is = %p\n",s + 1);//0x7ffde8cdf0ac /*可以发现使用p表示二维数组的元素和二维数组名表示元素是一样的用法*/ printf("*(*(p + 1)) is = %d\n",*(*(p + 1)));//*(*(p + 1)) is = 4 printf("*(*(s + 1)) is = %d\n",*(*(s + 1)));//*(*(s + 1)) is = 4 printf("*(p[1]+0) is = %d\n",*(p[1]+0));//*(p[1]+0) is = 4 printf("*(s[1]+1) is = %d\n",*(s[1]+1));//*(s[1]+1) is = 9 return 0; } // 一维数组: int num[2]; printf("%p \n",num); //0x7fffe2a8cde8 printf("%p \n",num+1); //0x7fffe2a8cdec printf("%p \n",&num+1); //0x7fffe2a8cdf0 // 可以看出: num +1 加的是一个Int &num + 1加的是整个数组 // 二维数组; int num[2][2]; printf("%p \n",num); //0x7ffe18f73560 printf("%p \n",num+1); //0x7ffe18f73568 printf("%p \n",&num+1); //0x7ffe18f73570 // 可以看出: num +1 加的是一个一维 &num + 1加的是整个数组 // 例子 int main() { int num[2][3]={1,2,3,4,5,6}; int (*p)[3]; p = num; printf("%p\n",p);//0x7ffffad71df0 printf("%p\n",p+1);//0x7ffffad71dfc printf("%p\n",num[0]+1);//0x7ffffad71df4 printf("%p\n",*p+1);//0x7ffffad71df4 // 注意这里&p+1 printf("%p\n",&num+1);//0x7ffffad71e08 printf("%d\n",**p);//1 }