【C语言】指针(1)——指针简单介绍及指针相关运算
目录1.内存和地址2.指针变量和地址2.1取地址操作符2.2指针变量和解引用操作符2.2.1指针变量2.2.2如何理解 int * p 如何拆解指针类型2.2.3解引用操作符*2.2.4指针变量的大小3.指针变量类型的意义3.1指针-整数3.2void*4.const修饰指针4.1 const修饰变量4.2 const修饰指针变量5.指针运算5.1指针-指针5.2指针-指针5.3指针的关系运算6.野指针6.1野指针成因1.指针未初始化2.指针越界访问3.指针指向的空间释放6.2如何规避野指针6.2.1指针初始化6.2.2小心指针越界6.2.4避免返回局部变量的地址7.assert断言8.传值调用和传址调用1.内存和地址计算机中最小的单位是比特位bit可以用来存储1个二进制位1byte 8bit 1KB 1024byte 1MB 1024KB 1GB 1024MB 1TB 1024GB 1PB 1024TB内存单元编号 地址 指针计算机中的编址不是把每个字节的地址记录下来而是通过硬件设计完成的。地址总线32位机器有32根地址总线可以表示2^32种含义每种含义表示一个地址。2.指针变量和地址2.1取地址操作符在C语言中创建变量的本质就是向内存申请空间。%p可以用来打印地址使用取出地址单目操作符a取出的是a所占四个字节中较小的字节的地址#includestdio.h int main() { int a 0; // 为a申请4个字节的空间 int *p a; return 0; }2.2指针变量和解引用操作符2.2.1指针变量使用取地址操作符取出的地址是一个数值需要存储在指针变量中。上述代码中p是一个指针变量。指针变量也是变量是用来存放地址的存放在指针变量中的值会被理解为地址。指针地址指针变量存放地址的变量口语中的指针一般指指针变量2.2.2如何理解 int * p 如何拆解指针类型*说明p是一个指针变量int说明p指向的对象是int类型同样的若p指向的类型是char类型定义时要使用char*2.2.3解引用操作符*解作符*又叫间接访问操作符*p表示通过p存放的地址找到p指向的对象可以通过*p间接访问p指向的对象例如可以通过*p0将p指向的对象的值变为0*和是可以相抵消的*a a2.2.4指针变量的大小32位机器假设有32根地址总线每根地址总线的电信号转换成数字信号后都表示0或1如果将32根地址总线产生的二进制序列当做一个地址那么该地址就需要32个bit位进行存储即32位机器的地址变量的大大小为4个字节。同理64位机器中的地址变量的大小为8个字节指针变量的大小与其所指向的对象类型无关只要是指针变量在相同平台上的大小都是相同的。如图在x86即32位环境下各种类型的指针变量大小均为4字节在x64即64位环境下均为8字节。3.指针变量类型的意义既然在同一个平台下各种类型的指针变量的大小是相同的为什么还要有各种各样的指针类型呢因为指针类型决定了对指针进行解引用操作时有多大的权限。比如char*指针在解引用时只能访问一个字节int*指针解引用时可以访问4个字节。3.1指针-整数#includestdio.h int main() { int a 10; int* pa a; char* pc (char*) a; printf(pa %p\n, pa); printf(pa1 %p\n, pa1); printf(pc %p\n, pc); printf(pc1 %p\n, pc1); return 0; }运行结果可以看出当int*类型的pa进行1操作时地址由006FFC08变为006FFC0C向后移动了4个字节sizeof(int)而char*类型的pc1时由006FFC08变为006FFC09向后移动了1个字节sizeof(char)。这就是不同指针变量类型的差别。结论指针类型决定了指针向前或向后一步有多大字节。3.2void*在指针类型中有一种特殊类型是void*类型的可以理解为无具体类型的指针或泛型指针。这种指针可以用来接受各种类型的地址但是也有局限性void*类型的指针不能直接进行指针-整数和解引用的运算。一般情况下void*类型的指针使用在函数参数的部分用来接收不同数据类型的地址可以实现泛型编程的效果使一个函数可以处理多种类型的数据。4.const修饰指针4.1 const修饰变量const int a 10; // a具有了常属性不能被修改在c语言中虽然a不能被修改了但本质上还是变量常变量在c中const修饰的变量就是常量被const修饰的变量可以通过指针修改破坏规则4.2 const修饰指针变量//代码1 void test1() { int n 10; int m 20; int* p n; *p 20;//ok? p m; //ok? } void test2() { //代码2 int n 10; int m 20; const int* p n; *p 20;//报错 p m; //ok? } void test3() { int n 10; int m 20; int* const p n; *p 20; //ok? p m; //报错 } void test4() { int n 10; int m 20; int const* const p n; *p 20; //报错 p m; //报错 } int main() { //测试⽆const修饰的情况 test1(); //测试const放在*的左边情况 test2(); //测试const放在*的右边情况 test3(); //测试*的左右两边都有const test4(); return 0; }一般来说const可以放在*的左边也可以放在*的右边当const放在*左边修饰的是指针指向的内容确保指针指向的内容不能通过指针改变但指针变量内容本身可以改变。当const放在*右边修饰的是指针变量本身保证了指针变量本身的内容不能修改但是指针指向的内容可以通过指针改变。也可以同时放在*左边和右边表示指针指向的内容和指针变量本身均不能修改。5.指针运算指针运算有三种分别是1指针-指针2指针-指针3指针的关系运算5.1指针-指针因为数组在内存中是连续存放的所以只要知道第一个元素的地址就可以找到后面所有元素。Type * p;p1 -- 跳过 1*sizeof(type)pn -- 跳过 n*sizeof(type)#includestdio.h int main() { int arr[10] { 0,1,2,3,4,5,6,7,8,9 }; int* p arr[0]; int sz sizeof(arr) / sizeof(arr[0]); for (int i 0; i sz; i) { printf(%d\n, *(p i)); } return 0; }以上代码可用于输出数组元素5.2指针-指针|指针-指针| 两指针间的元素个数前提两指针指向同一块空间如指向同一个数组不存在指针指针无意义5.3指针的关系运算指针的关系运算实际上就是指针和指针比较大小可以用来判断指针是否指向数组内、6.野指针概念野指针就是指针指向的位置是不可知的随机的不正确的没有明确限制的6.1野指针成因1.指针未初始化指针变量未初始化默认为随机值2.指针越界访问eg.当指针指向的范围超出数组范围时就成为野指针3.指针指向的空间释放eg.函数结束后指针指向的变量被释放p成为野指针6.2如何规避野指针6.2.1指针初始化指针初始化有两种情况1.明确知道指针应该指向哪里初始化一个明确的地址2.暂时不知道指针应该指向哪里初始化为NULLNULL是C语言中定义的标识符常量值为0,0也是地址这个地址是无法使用的读写该地址会报错6.2.2小心指针越界一个程序向内存申请了多少空间通过指针也就只能访问那些空间不能超出访问范围超出了就是越界访问。6.2.3指针变量不再使用时及时置为NULL指针使用前检查有效性。不再使用某个指针时要将指针置为NULL使用指针前先判断指针是否为NULL6.2.4避免返回局部变量的地址7.assert断言assert.h头文件定义了宏assert()用于在运行时确保特定程序符合指定条件如果不符合就报错终止运行这个宏常被称为“断言”。assert(p) //判断p是否为空指针等价于assert(p!NULL)assert()宏接受一个表达式作为参数任意表达式。如果该表达式为真返回值非零assert()不会有任何反应程序继续运行如果该表达式为假返回值为零assert()就会报错在标准错误流stderr中写入一条错误信息显示错误的表达式及其所在的文件名及行号。使用assert()的好处1.可以自动标识文件和出问题的行号2.如果已经确保程序没有问题不再需要assert()断言就在#includeassert.h前定义一个宏NDEBUG。然后重新编译编译器就会自动禁用文件中的所有assert()语句。#define NDEBUG #includeassert.hassert()的缺点因为引入了外部的检查增加了程序的运行时间。在vs中Release版本直接将assert()优化掉了在linux版本中Release版本仍会起作用。8.传值调用和传址调用传值调用把变量本身传递给函数。实参传递给形参时形参会单独创建一份临时空间来接收实参对形参的修改不会影响实参。传址调用把变量的地址传递给函数。可以让函数和主调函数之间建立真正的联系在函数内部可以修改主调函数中的变量。如果函数中只是需要主调函数中的变量值进行运算可以采用传值调用如果函数内部要修改主调函数变量的值就要采用传址调用。完