C语言----深入理解指针(1)(一)https://developer.aliyun.com/article/1544335
5.指针运算
指针的基本运算有三种,分别是:
指针+-指数
指针-指针
指针的关系运算
//循环打印数组的内容 //int main() //{ // int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // int sz = sizeof(arr) / sizeof(arr[0]); // for (int i = 0; i < sz; i++) // { // printf("%d", arr[i]); // } // // // return 0; // //} //采用指针来获取数组元素的地址 //int main() //{ // int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // int sz = sizeof(arr) / sizeof(arr[0]); // int *p = &arr[0];//将arr[0]的地址存在*p中 // for (int i = 0; i < sz; i++) // { // printf("%d ", *p);//解引用来打印arr[0] // p++;//打印完p++往后走一步,整型指针加一就是向后挪了一个整型 // //循环十次就能把这个数组的内容打印出来 // } // return 0; //} //获取数组第一个数字的地址赋值给p,再利用*p解引用,打印*p所指的数 //p+1就是*(p+1),打印数组下一个数字 //1.指针类型决定了指针+1的步长,决定了指针解引用的权限 //2.数组在内存中是连续存放的 //int main() //{ // int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // int sz = sizeof(arr) / sizeof(arr[0]); // int* p = &arr[0];//将arr[0]的地址存在*p中 // for (int i = 0; i < sz; i++) // { // printf("%d ", *(p + i));//直接解引用*(p+i),当i=0时,就是*p,打印的就是数组第一个数 // // } // return 0; //} p+i 是跳过i*sizeof(int)个字节
//指针-整数,从10开始打印 //int main() //{ // int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // int sz = sizeof(arr) / sizeof(arr[0]); // int* p = &arr[sz-1];//数组中最后一位的下标是sz-1 // for (int i = 0; i < sz; i++) // { // printf("%d ", *p ); // p--; // } // return 0; //}
//指针-指针的绝对值是指针和指针之间元素的个数 //指针-指针,计算的前提条件是两个指针指向的是同一块空间 //int main() //{ // int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // printf("%d\n", &arr[9] - &arr[0]);//输出结果是9 // printf("%d\n", &arr[0]-&arr[9] );//输出结果是-9 // return 0; //} //size_t my_strlen(char* p)//传过来的是数组名,用字符串指针来接收,*p指向的就是数组第一个元素 //{//size_t是无符号返回值 // size_t count = 0; // while (*p != '\0') // { // count++; // p++;//往后走一位 // } // return count; //} //int main() //{ // char arr[] = "abcdef"; // size_t len = my_strlen(arr);//数组名其实是数组首元素的地址,arr==&arr[0] // //传过去数组名,就是传过去首元素的地址 // printf("%zd\n", len);//打印结果是6 // // return 0; //} //另一种写法 size_t my_strlen(char* p)//传过来的是数组名,用字符串指针来接收,*p指向的就是数组第一个元素 {//size_t是无符号返回值 char* star = p;//指向的是数组第一个数字 char* end = p; while (*end!= '\0')//如果end不等于\0,就让end++ {//这个while的循环条件可以是while(*end),因为到了\0的时候,\0的ASCLL值就是0,不满足循环条件就停下来了 end++;//直到enf走到\0不满足条件就不进行循环了,此时的*end指向的就是\0 } return end-star;//两个指针相减得到的就是指针之间元素的个数 }//数组中最后一个元素的指针减去第一个指针的元素得到的 就是这个数组的数量 int main() { char arr[] = "abcdef"; size_t len = my_strlen(arr);//数组名其实是数组首元素的地址,arr==&arr[0] //传过去数组名,就是传过去首元素的地址 printf("%zd\n", len);//打印结果是6 return 0; }
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//数组中随着下标的增长,地址由低到高变化的 int sz = sizeof(arr) / sizeof(arr[0]); int* p = arr[0];//*p指向数组首个元素 while (p < &arr[sz])//数组下标为10的数不在数组之内,所以在arr[sz]前面的就是数组所有的元素 {//p的地址大小小于arr[sz]的地址,所以只要地址一直小于arr[sz]的地址就一直可以循环打印 printf("%d ", *p); p++; } return 0; } //这里就运用到指针关系大大小p < &arr[sz]
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
//1.未初始化会造成野指针 /*int main() { //一个局部变量不初始化的话,它的值是随机的 int* p;//p是局部变量,没有初始化,其值是随机值,如果将p中的值当做地址, //解引用操作就会形成非法访问 *p = 20;//p就是野指针 return 0; }*/ //2.指针越界访问也会造成野指针的问题 /*int main() { int arr[10] = { 0 };//数组初始化 int i = 0; int* p = &arr[0];//将数组第一个元素的地址给p for (i = 0; i <= 10; i++)//循环11次 { *p = i;//当第11次循环的时候,访问到了不属于这个数组的空间,访问到数组之外的空间了 p++;//此时的p就是野指针了 } return 0; }*/ //3.指针指向的空间释放也会造成野指针 int test() { int n = 100;//n是局部变量,进入函数创建,出函数销毁,也就是说返回的&n的地址并不是原先存储100的地址 return &n;//地址被还给操作系统了 } int main() { int* p = test();// printf("%d\n", *p);//p一旦接受这个地址,p里面的就是野指针,造成内存非法访问,篡改内存 return 0; }
//规避野指针 //如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里, // 可以给指针赋值NULL //NULL是c语言中定义的一个表示符常量,值是0,0也是地址,这个地址是无法正常使用的,读写地址会报错 /*int main() { int a = 10; int* p = &a;//给出明确的地址,将a的地址赋值给p int* p2 = NULL;//把野狗拴在柱子上。p2没有指向的对象 return 0; }*/ //int main() //{ // int* p = NULL; // if (p != NULL) // { // *P = 200; // } // return 0; // //} //当指针变量指向一块区域时,我们可以通过指针访问该区域, // 后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL //只要是空指针我们就不去访问,类似把野狗用柱子拴起来,将野指针暂时管理起来
如何规避野指针:
1.对指针进行初始化
2.小心指针越界
3.指针变量不再使用,及时置NULL(空指针),指针使用之前检查有效性
4.避免返回局部变量的地址
7.assert断言
assert.h头文件定义了assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”
#define NDEBUG int main() { /*int* p = NULL; assert(p != NULL);*///会报错,assert判断后面括号的条件,为假就报错 int a = 10; int* p = &a; assert(p != NULL);//这种情况就不会报错 //assert可以判断指针的有效性 //#define NDEBUG,利用这句话就可以控制assert,是否产生效果 //如果想产生效果就注释掉,不想产生效果就在#include <assert.h>上方添加 //只要添加了#define NDEBUG这个语句,代码中的assert就会被禁用 //assert()语句的缺点就是,因为引入了额外的检查,增加了程序的运行时间 /*if (p != NULL) { *P = 200; } return 0; }*/ //在vs版本中,Debug中assert()语句是可以使用的,但是在Release版本中直接优化掉了assert()语句 //这样debug版本编写代码有利于程序员排查问题,在Release版本不影响用户使用时的效率 //在Release版本选择性的优化assert()
8.指针的使用和地址调用
//strlen是求字符长度的,统计的是字符串中\0之前的字符个数 //函数求字符串长度 //参数s指向的字符串不期望被修改 size_t my_strlen(const char*s)//把字符元素的地址传过来,用char*s接收 {//添加const不希望字符串被修改,直接将每次传来的实参固定死 //不加const的话原先字符串的长度就被修改了 size_t count = 0; assert(s != NULL);//防止传过来的实参为空指针,检测指针s是否有效 while (*s)//当s遇到\0的时候,循环就停止 { count++; s++; } return count; } int main() { char arr[] = "abcdef"; size_t len = my_strlen(arr); printf("%zd", len); return 0; }
//写一个函数,交换两个整型变量的值 //void Swap1(int x, int y) //{ // int z = 0; // z = x;//先把x的值放到z里面,x空了 // x = y;//把y的值放到x里面,y空了 // y = z;//把z的值放到y里面去,在这之前放在z里面的值是x //} // // //int main() //{ // int a = 0; // int b = 0; // scanf("%d %d", &a, &b); // // //交换a和b的值 // printf("交换前:a=%d b=%d\n", a, b); // Swap1(a, b); // printf("交换后:a=%d b=%d\n", a, b); // return 0; //} //改代码打印结果是: //交换前:a=3 b=5 //交换后:a = 3 b = 5 //很明显,出问题了 //当实参传递给形参的时候,形参是实参的一份临时拷贝, //对形参的修改不会影响实参 //那么如何修改呢? void Swap2(int *pa, int*pb) { int z = 0; z = *pa;//z=a *pa = *pb;//a=b *pb = z;//b=z }//脑海中把图画出来 int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); //交换a和b的值 printf("交换前:a=%d b=%d\n", a, b); Swap2(&a, &b);//把a、b的地址传过去 printf("交换后:a=%d b=%d\n", a, b); return 0; } //在这两个代码中,Swap1是传值调用 //Swap2是传址调用,直接将变量本身传递过去了 //当我们采用的是传值调用,形参和实参占用的是不同的空间,对形参的修改不会改变实参 //完成两个整数的相加 Add(int x,int y) { int z = x + y; return z; } int main() { int a = 10; int b = 20; int c=Add(a, b); printf("%d\n", c); //传值调用 return 0; } //当使用传值调用时,实际上是将参数值复制到函数内部的一个局部变量中。 // 这意味着函数内部对参数值所做的任何修改都不会影响原始变量。 //原始数据不会被修改,传值调用通常被认为是安全的 //传址调用涉及将参数的内存地址传递给函数。这意味着函数可以直接访问和修改原始变量。
传值调用:实际上是将参数值复制到函数内部的一个局部变量中,这意味着函数内部对参数值所做的任何修改都不会影响原始变量,原始数据不会被修改
传址调用:涉及将参数的内存地址传递给函数,这意味着函数可以直接访问和修改原始变量。