cms 美容网站 模版,网页制作与设计在工作中的应用,优化大师官网下载安装,做网站怎么做多少钱目录
内存和地址
指针变量和地址
取地址#xff08;#xff09;
解引用#xff08;*#xff09;
大小
类型
意义
const修饰
修饰变量
修饰指针
指针运算
指针- 整数
指针-指针
指针的关系运算
野指针
概念
成因
避免
assert断言
指针的使用
strl…目录
内存和地址
指针变量和地址
取地址
解引用*
大小
类型
意义
const修饰
修饰变量
修饰指针
指针运算
指针- 整数
指针-指针
指针的关系运算
野指针
概念
成因
避免
assert断言
指针的使用
strlen的模拟实现
传值调用和传址调用
指针和数组
特殊情况
指针访问
指针数组
模拟⼆维数组 二级指针 在开始正式介绍之前我先提一点就是计算机中常⻅的单位 1Byte比特位 8bit字节 1KB 1024Byte 1MB 1024KB 1GB 1024MB 1TB 1024GB 1PB 1024TB 以下的代码我均是在32位平台下地址运行的代码因为32位通过调试观察会更清晰 内存和地址 内存空间是把内存划分为⼀个个的内存单元每个内存单元的⼤⼩取1个字节。其中每个内存单元相当于⼀个学⽣宿舍⼀个字节空间⾥⾯能放8个⽐特位就好⽐同学们住的⼋⼈间每个⼈是⼀个⽐特位。每个内存单元也都有⼀个编号这个编号就相当于宿舍房间的⻔牌号。⽣活中我们把⻔牌号也叫地址在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫指针。 所以我们可以理解为内存单元的编号 地址 指针 指针变量和地址
取地址 我们可以通过取地址操作符取出变量的内存起始地址把地址可以存放到一个变量中这个变量就是指针变量 #include stdio.h
int main()
{int a 10;//在内存中开辟一块空间int* p a;//这里我们对变量a取出它的地址可以使用操作符。//a变量占用4个字节的空间这里是将a的4个字节的第一个字节的地址存放在p变量中//p就是一个指针变量。return 0;
} 上述的代码就是创建了整型变量a内存中申请4个字节⽤于存放整数10其中每个字节都有地址上图中4个字节的地址分别是 0x0093F7FC 0x0093F7FD 0x0093F7FE 0x0093F7FF 解引用* 我们只要拿到了地址指针就可以通过地址指针找到地址指针指向的对象而实现这个操作就需要借助⼀个操作符叫解引⽤操作符(*)。 #include stdio.h
int main()
{int a 100;int* p a;*p 0;return 0;
} 上⾯代码中第5⾏就使⽤了解引⽤操作符*p 的意思就是通过p中存放的地址找到指向的空间*p其实就是a变量了所以*p 0这个操作符是把a改成了0其实这⾥是把a的修改交给了p来操作这样对a的修改就多了⼀种的途径写代码就会更加灵活往后学习就会知道指针非常香 大小
32位机器假设有32根地址总线每根地址线出来的电信号转换成数字信号后是1或者0那我们把32根地址线产⽣的2进制序列当做⼀个地址那么⼀个地址就是32个bit位需要4个字节才能存储。
如果指针变量是⽤来存放地址的那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器假设有64根地址线⼀个地址就是64个⼆进制位组成的⼆进制序列存储起来就需要8个字节的空间指针变量的⼤⼩就是8个字节 总结指针变量的⼤⼩取决于地址的⼤⼩ 32位平台下地址是32个bit位指针变量⼤⼩是4个字节 64位平台下地址是64个bit位指针变量⼤⼩是8个字节 注意指针变量的⼤⼩和类型是⽆关的只要指针类型的变量在相同的平台下⼤⼩都是相同的。 类型
//我们如何理解指针的类型呢
int a 10;
int * pa a; 这里p左边写的是int* 是在说明p是指针变量而前面的int是在说明p指向的是整型(int)类型的对象。那如果要存放一个char类型的变量ch呢那就要用char 类型的指针变量同理其它类型也如此 char *pc NULL;
int *pi NULL;
short *ps NULL;
long *pl NULL;
float *pf NULL;
double *pd NULL; 可以看出指针的定义方式是 type *那就是说 char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放 int 类型变量的地址。 意义
指针变量的⼤⼩和类型⽆关只要是指针变量在同⼀个平台下⼤⼩都是⼀样的为什么还要有各种各样的指针类型呢
① 指针的解引用 通过调试我们可以看到代码1会将n的4个字节全部改为0但是代码2只是将n的第⼀个字节改为0。那我们就知道char* 的指针解引⽤就只能访问⼀个字节⽽ int* 的指针的解引⽤就能访问四个字节。 结论指针的类型决定了对指针解引⽤的时候有多⼤的权限⼀次能操作⼏个字节。 ②指针-整数 通过这段代码我们可以看出 char* 类型的指针变量1跳过1个字节 int* 类型的指针变量1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针1其实跳过1个指针指向的元素。指针可以1那也可以-1。 结论指针的类型决定了指针向前或者向后⾛⼀步有多⼤距离。 ③ void* 指针 在指针类型中有⼀种特殊的类型是 void * 类型的可以理解为⽆具体类型的指针或者叫泛型指针这种类型的指针可以⽤来接受任意类型地址。但是也有局限性 void* 类型的指针不能直接进⾏指针的-整数和解引⽤的运算。 在上⾯的代码中将⼀个int类型的变量的地址赋值给⼀个char类型的指针变量。编译器给出了⼀个警告是因为类型不兼容。⽽使⽤void类型就不会有这样的问题。
使⽤void*类型的指针接收地址 这⾥我们可以看到 void* 类型的指针可以接收不同类型的地址但是⽆法直接进⾏指针运算。 void* 类型指针的作用 ⼀般 void* 类型的指针是使⽤在函数参数的部分⽤来接收不同类型数据的地址这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据void* 类型还是很香的后面介绍会用到。 const修饰
修饰变量
变量是可以修改的如果把变量的地址交给⼀个指针变量通过指针变量的也可以修改这个变量。但是如果我们希望⼀个变量加上⼀些限制不能被修改怎么做呢这就是const的作⽤。
#include stdio.h
int main()
{int a 0;a 20;//a是可以修改的const int b 0;b 20;//b是不能被修改的return 0;
}
上述代码中b是不能被修改的其实b本质是变量只不过被const修饰后在语法上加了限制只要我们在代码中对b进⾏修改就不符合语法规则就报错致使没法直接修改b。
int main()
{const int b 0;printf(b %d\n, b);int*p b;*b 20;printf(b %d\n, b);return 0;
}
但是如果我们绕过b使⽤b的地址去修改b就能做到了但是这样做是在打破语法规则是不合理的我们的初衷就是为了b不能被修改那么这时候就需要用const修饰指针。
修饰指针
① const放在*的左边情况
#include stdio.h
int main()
{int n 10;int m 20;const int* p n;//编译器报错表达式必须是可修改的左值*p 20;p m; return 0;
}
② const放在*的右边情况
int main()
{int n 10;int m 20;int* const p n;*p 20; //编译器报错表达式必须是可修改的左值p m;return 0;
}
③ const放在*的左右两边情况
#include stdio.h
int main()
{int n 10;int m 20;int const* const p n;//编译器报错表达式必须是可修改的左值*p 20; //编译器报错表达式必须是可修改的左值p m;return 0;
} 结论const修饰指针变量的时候 ① const如果放在*的左边修饰的是指针指向的内容保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。 ② const如果放在*的右边修饰的是指针变量本⾝保证了指针变量的内容不能修改但是指针指向的内容可以通过指针改变。 指针运算
指针- 整数 因为数组在内存中是连续存放的只要知道第⼀个元素的地址顺藤摸⽠就能找到后⾯的所有元素。 #include stdio.h
int main()
{int arr[10] { 1,2,3,4,5,6,7,8,9,10 };int* p arr[0];int i 0;int sz sizeof(arr) / sizeof(arr[0]);//数组下标是从0开始的for (i 0; i sz; i){printf(%d , *(p i));//pi 这⾥就是指针整数}return 0;
}
指针-指针
#include stdio.h
int my_strlen(char* s)
{char* p s;while (*p ! \0)p;return p - s;
}
int main()
{printf(%d\n, my_strlen(abc));return 0;
}
指针的关系运算
#include stdio.h
int main()
{//数组名是数组首元素的地址int arr[10] { 1,2,3,4,5,6,7,8,9,10 };int* p arr[0];int sz sizeof(arr) / sizeof(arr[0]);while (p arr sz) //指针的⼤⼩⽐较{printf(%d , *p);p;}return 0;
} 野指针
概念 野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的 成因
① 指针未初始化
#include stdio.h
int main()
{ int *p;//局部变量指针未初始化默认为随机值*p 20;return 0;
}
② 指针越界访问
#include stdio.h
int main()
{int arr[10] {0};int *p arr[0];int i 0;for(i0; i11; i){//当指针指向的范围超出数组arr的范围时p就是野指针*(p) i;}return 0;
}
③ 指针指向的空间释放
int* test()
{int n 100;return n;
}
int main()
{int*p test();printf(%d\n, *p);return 0;
} 避免
野指针的成因大多数都与我们不规范的语法有关有没有办法避免呢答案肯定是有的。
① 指针初始化 如果明确知道指针指向哪⾥就直接赋值地址如果不知道指针应该指向哪⾥可以给指针赋值NULLNULL 是C语⾔中定义的⼀个标识符常量值是00也是地址这个地址是⽆法使⽤的读写地址会报错 //NULL实际是⼀个宏在传统的C头⽂件(stddef.h)中可以看到如下代码
#ifdef __cplusplus#define NULL 0
#else#define NULL ((void *)0)
#endif
//初始化
#include stdio.h
int main()
{int num 10;int*p1 num;int*p2 NULL;return 0;
}
② 小心指针越界 ⼀个程序向内存申请了哪些空间通过指针也就只能访问哪些空间不能超出范围访问超出了就是越界访问。使用数组时特别要注意这点 ③ 检查有效性 当指针变量指向⼀块区域的时候我们可以通过指针访问该区域后期不再使⽤这个指针访问空间的时候我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是只要是NULL指针就不去访问同时使⽤指针之前可以判断指针是否为NULL。 我们可以把野指针想象成野狗野狗放任不管是⾮常危险的所以我们可以找⼀棵树把野狗拴起来就相对安全了给指针变量及时赋值为NULL其实就类似把野狗栓起来就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛不能去挑逗野狗有点危险对于指针也是在使⽤之前我们也要判断是否为NULL看看是不是被拴起来起来的野狗如果是不能直接使⽤如果不是我们再去使⽤。
#include stdio.h
int main()
{int arr[10] {1,2,3,4,5,6,7,8,9,10};int *p arr[0];int i 0;for(i0; i10; i){*(p) i;}//此时p已经越界了可以把p置为NULLp NULL;//下次使⽤的时候判断p不为NULL的时候再使⽤//...p arr[0];//重新让p获得地址if(p ! NULL) //判断{//...}return 0;
}
④ 小心局部变量 这和我们成因的第三点相关因为函数执行完就会释放即使返回了一个指针但指针指向什么谁都不知道所以返回的是一个野指针这点也是特别要小心的。 assert断言
我觉得这个东西有必要介绍给大家因为这个使用起来会特别香特别是到数据结构阶段那时候会经常使用assert断言。 assert.h 头⽂件定义了宏 assert() ⽤于在运⾏时确保程序符合指定条件如果不符合就报错终⽌运⾏。这个宏常常被称为“断⾔”。 assert(p ! NULL); 上⾯代码在程序运⾏到这⼀⾏语句时验证变量 p 是否等于 NULL 。如果确实不等于 NULL 程序继续运⾏否则就会终⽌运⾏并且给出报错信息提⽰。 assert() 宏接受⼀个表达式作为参数。如果该表达式为真返回值⾮零 assert() 不会产⽣任何作⽤程序继续运⾏。如果该表达式为假返回值为零 assert() 就会报错在标准错误流 stderr 中写⼊⼀条错误信息显⽰没有通过的表达式以及包含这个表达式的⽂件名和⾏号。 assert() 的使⽤对程序员是⾮常友好的使⽤ assert() 有⼏个好处它不仅能⾃动标识⽂件和出问题的⾏号还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题不需要再做断⾔就在 #include assert.h 语句的前⾯定义⼀个宏 NDEBUG 。 #define NDEBUG
#include assert.h 然后重新编译程序编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题可以移除这条 #define NDEBUG 指令或者把它注释掉再次编译这样就重新启⽤了 assert() 语句。 assert() 的缺点是因为引⼊了额外的检查增加了程序的运⾏时间。⼀般我们可以在 Debug 中使⽤在 Release 版本中选择禁⽤ assert 就⾏在 VS 这样的集成开发环境中在 Release 版本中直接就是优化掉了。这样在debug版本写有利于程序员排查问题在 Release 版本不影响⽤⼾使⽤时程序的效率。 指针的使用
前面介绍了那么多下面我将介绍实际点的应用
strlen的模拟实现
库函数strlen的功能是求字符串⻓度统计的是字符串中 \0 之前的字符的个数。
//函数原型如下
size_t strlen ( const char * str ); 参数str接收⼀个字符串的起始地址然后开始统计字符串中 \0 之前的字符个数最终返回⻓度。如果要模拟实现只要从起始地址开始向后逐个字符的遍历只要不是 \0 字符计数器就1这样直到 \0 就停⽌。 //模拟实现
int my_strlen(const char* str)
{int count 0;assert(str);while (*str){count;str;}return count;
}
int main()
{int len my_strlen(abcdef);printf(%d\n, len);return 0;
} 传值调用和传址调用
我们先来看下面一段代码
#include stdio.h
void Swap1(int x, int y)
{int tmp x;x y;y tmp;
}
int main()
{int a 1;int b 3;printf(交换前a%d b%d\n, a, b);Swap1(a, b);printf(交换后a%d b%d\n, a, b);return 0;
}
我们的目的是交换两个数的值但是我们运行代码发现它们两个值并没有发生变化。这是为什么呢代码看上去明明没有问题啊。 我们调试起来分别取它们的地址发现原来在Swap1函数内部创建了形参x和y接收a和b的值但是x的地址是0x00b9f854y的地址是0x00b9f858x和y确实接收到了a和b的值不过x的地址和a的地址不⼀样y的地址和b的地址不⼀样相当于x和y是独⽴的空间那么在Swap1函数内部交换x和y的值⾃然不会影响a和b当Swap1函数调⽤结束后回到main函数a和b的没法交换。 Swap1函数在使⽤的时候是把变量本⾝直接传递给了函数这种调⽤函数的⽅式就是传值调⽤。 这时候就要借助指针了我们现在要解决的就是当调⽤Swap函数的时候Swap函数内部操作的就是main函数中的a和b直接将a和b的值交换了。那么如果我们在main函数中将a和b的地址传递给Swap函数Swap函数⾥边通过地址间接的操作main函数中的a和b是不是就达到交换的效果。 #include stdio.h
void Swap2(int* px, int *py)
{int tmp *px;*px *py;*py tmp;
}
int main()
{int a 1;int b 3;printf(交换前a%d b%d\n, a, b);Swap2(a, b);printf(交换后a%d b%d\n, a, b);return 0;
} 我们再次运行代码终于显示a和b完成了交换。我们可以看到实现成Swap2的⽅式顺利完成了任务这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数这种函数调⽤⽅式叫传址调⽤。 我们再次调试起来发现px存储的是a的地址py存储的是b的地址当我们解引用操作就相当于间接对a和b进行操作。 传址调⽤可以让函数和主调函数之间建⽴真正的联系在函数内部可以修改主调函数中的变量所以未来函数中只是需要主调函数中的变量值来实现计算就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值就需要传址调⽤。 总结 ①如果需要被改动则需要传递指向这个参数的指针。 ②如果不用被改动可以直接传递这个参数。 大家一定要搞清这个点因为后续的内容也会涉及到特别特别重要 指针和数组
特殊情况
我们先来看下面这段代码 我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样所以数组名就是数组⾸元素(第⼀个元素)的地址。 但是有两种情况数组名代表的不是数组首元素的地址。
#include stdio.h
int main()
{int arr[10] { 1,2,3,4,5,6,7,8,9,10 };printf(%d\n, sizeof(arr));return 0;
}
输出的结果是40如果arr是数组⾸元素的地址那输出应该的应该是4/8才对。 ① sizeof(数组名)sizeof中单独放数组名这⾥的数组名表⽰整个数组计算的是整个数组的⼤⼩单位是字节 ② 数组名这⾥的数组名表⽰整个数组取出的是整个数组的地址整个数组的地址和数组⾸元素的地址是有区别的 除此之外任何地⽅使⽤数组名数组名都表⽰⾸元素的地址。 #include stdio.h
int main()
{int arr[10] { 1,2,3,4,5,6,7,8,9,10 };printf(arr[0] %p\n, arr[0]);printf(arr %p\n, arr);printf(arr %p\n, arr);return 0;
}
大家运行这段代码会发现三个打印结果⼀模⼀样但我刚刚不是说代表的整个数组的地址吗我们接着往下看
#include stdio.h
int main()
{int arr[10] { 1,2,3,4,5,6,7,8,9,10 };printf(arr[0] %p\n, arr[0]);printf(arr[0]1 %p\n, arr[0] 1);printf(arr %p\n, arr);printf(arr1 %p\n, arr 1);printf(arr %p\n, arr);printf(arr1 %p\n, arr 1);return 0;
} 输出结果 arr[0] 0077F820 arr[0]1 0077F824 arr 0077F820 arr1 0077F824 arr 0077F820 arr1 0077F848 这⾥我们发现arr[0]和arr[0]1相差4个字节arr和arr1 相差4个字节是因为arr[0] 和 arr 都是⾸元素的地址1就是跳过⼀个元素。但是arr 和 arr1相差40个字节这就是因为arr是数组的地址1 操作是跳过整个数组的。 想必⼤家应该搞清楚数组名的意义了。 指针访问 通过代码运行的结果可以发现 pi 其实计算的是数组 arr 下标为i的地址。那么我们就可以直接通过指针来访问数组。
#include stdio.h
int main()
{int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };int* p arr; //指针存放数组首元素的地址int sz sizeof(arr) / sizeof(arr[0]);int i 0;for (i 0; i sz; i){printf(%d , *(p i));}return 0;
} 另外要提一点就是 ① arr[i]等价于*(arri) ① arri等价于((arri)j) 指针数组
指针数组是指针还是数组我们类⽐⼀下整型数组是存放整型的数组字符数组是存放字符的数组。那指针数组呢那不就是存放指针的数组。 指针数组的每个元素是地址⼜可以指向⼀块区域。 模拟⼆维数组
#include stdio.h
int main()
{int arr1[] { 1,2,3,4,5 };int arr2[] { 2,3,4,5,6 };int arr3[] { 3,4,5,6,7 };//数组名是数组⾸元素的地址类型是int*的就可以存放在parr数组中int* parr[3] { arr1, arr2, arr3 };int i 0;int j 0;for (i 0; i 3; i){for (j 0; j 5; j){printf(%d , parr[i][j]);}printf(\n);}return 0;
} parr[i]是访问parr数组的元素parr[i]找到的数组元素指向了整型⼀维数组parri就是整型⼀维数组中的元素。 虽然上面的代码模拟出⼆维数组的效果但实际上并⾮完全是⼆维数组因为每⼀⾏并⾮是连续的。 二级指针
指针变量也是变量是变量就有地址那指针变量的地址存放在哪里这时候就需要借助二级指针了。 对于⼆级指针的运算有 ① *ppa 通过对ppa中的地址进⾏解引⽤这样找到的是 pa *ppa 其实访问的就是 pa int b 20;
*ppa b;//等价于 pa b; ② **ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作 *pa 那找到的是 a **ppa 30;
//等价于*pa 30;
//等价于a 30;