本文主要是介绍你不知道的C语言知识(第四期:指针【1】),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本期介绍🍖
主要介绍:C语言中一些大家熟知知识点中的盲区,这是第四期,主讲指针方面。
文章目录
- 1. 内存和地址
- 1.1 内存
- 1.2 该如何理解编址
- 2. 指针类型的意义
- 2.1 指针的解引用
- 2.2 指针加减整数
- 3. const修饰
- 3.1 const修饰变量
- 3.2 const修饰指针变量
- 4. 指针运算
- 5. 野指针
- 5.1 指针未初始化
- 5.2 指针越界访问
- 5.3 指针指向的空间被释放
- 6. assert断言
- 7. 传值调用和传址调用
- 7.1 传值调用
- 7.2 传址调用
1. 内存和地址
1.1 内存
内存是计算机中一个非常重要的存储器,为系统、软件、程序的运行提供了足够的空间。值得注意的是,内存只用于暂时性的存放数据和程序,一旦关闭电源或发生断电,其中的程序和数据将会丢失。
我们知道CPU在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。当下买电脑的时候,内存的容量有8GB、16GB、32GB,面对这么大一块内存空间,该如何高效管理呢?
面对这么大的一块内存空间,不可能就只存放一个数据的,首先就需要将它划分成一个个小的内存单元,每个内存单元占1个字节的空间用于存放数据,如下图所示。想要准确的找到所需的内存单元该如何是好呢?。在生活中,快递公司想将包裹送到你家,必然需要有你家的地址。同理,只需给每个内存单元编号,通过这个编号就能找到该内存单元。
生活中我们把门牌叫做地址,同理在计算机所说的 “地址” 就是指中内存单元的编号,C语言中又给地址起了个新名字 “指针”。所以我们可以理解为:内存单元的编号 == 地址 == 指针。
1.2 该如何理解编址
计算机内有很多硬件单元,而硬件单元是要相互协同的(协同:指相互之间进行数据的交互)。但是硬件单元之间是相互独立的,故需要用 “线” 将它们连起来,CPU与内存之间就存在着3条线(地址总线,数据总线,控制总线),如下图所示。
其中控制总线用于传输CPU是要读取数据、写入数据,地址总线传输需要写入/读取的内存地址,数据总线最后再将数据进行传输,要么读入CPU中要么写入内存空间中。今天主要讲地址总线,思考一下:内存地址的编址是如何通过硬件来实现的呢?
地址总线具体有多少根线,可以从计算机处理器的位数上知晓,就譬如32位机器就有32根地址线,64位机器就有64根地址线。而每根地址线只有得电、失电两种,转换成数字量就是0、1。那么1根地址线能表示2种状态,2根地址线能表示4种状态,3根地址线能表示8种状态。以此类推,32根地址线就能够表示232种状态,每种状态都代表一个地址,那就是有232个地址,从0X00000000到0XFFFFFFFF(十六进制表示形式)。
值得注意:32位的地址需要用32个bit位来存放,即4个byte。那么64位的地址,自然需要使用64个bit位来存放,即8个byte。
2. 指针类型的意义
在相同的平台上,指针变量的大小与类型一点关系都没有。举例:在32位平台上char*
、int*
、double*
指针的大小都是4个字节。问:那为什么还需要存在各种各样的指针类型呢?重定义一个专门用与指针的类型不好吗?
2.1 指针的解引用
通过指针的*
(解引用操作符),可以访问到指针所指向的那个对象,那指针的类型存在的意义会不会在此呢?如下所示案例。
当在调试的时候观察内存窗口,会发现int*
的指针“解引用”一次能够修改4个字节,而char*
型的指针“解引用”一次只能够修改1个字节,如下图所示。
结论: 指针的类型决定了,指针在解引用操作的时候一次能够访问空间的大小。
2.2 指针加减整数
对指针进行+
、-
整数操作,也就是对地址进行+
、-
整数操作,是不是只增加/减少几个地址编号,也就是跳过一个内存单元(1个字节空间)?举例如下所示。
#include<stdio.h>
int main()
{int n = 10;int* pi = &n;char* pc = &n;printf(" &n = %p\n", &n);printf(" pi = %p\n", pi);printf("pi+1 = %p\n", pi + 1);printf(" pc = %p\n", pc);printf("pc+1 = %p\n", pc + 1);return 0;
}
可以看出,整型指针加1后跳过了4个字节,也就是一个整型变量,字符指针加1后跳过了1个字节,也就是一个字符变量。其实指针加1就是跳过了一个指针指向类型的元素。
结论: 指针的类型决定了指针向前或向后走一步有多大距离。
3. const修饰
3.1 const修饰变量
const
作为C语言中的一个关键字,一般用于修饰某个变量。当变量被const
修饰时,那么这个变量就拥有了恒定不变的属性。举例如下。
#include<stdio.h>
int main()
{const int a = 10;a = 100;return 0;
}
当想更改已经被const
修饰的变量,会发现编译器报错不让修改。这样变量a
不就变得跟常量一样了嘛,但这是否就意味着被const
修饰的变量就变为常量了?举例如下。
#include<stdio.h>
int main()
{int arr[10] = { 0 };const int n = 10;char str[n] = { 0 };return 0;
}
在创建数组时,数组[]
内必须是常量表达式,假设const
修饰后的变量会转换为常量,那么一定会得到:使用const修饰的变量去创建数组是能够通过。但真正去这么做的时候,编译器就报错了,它不认为const
修饰的变量是常量,从这可知const
修饰的变量任然是变量。
结论: const
只是赋予变量常属性,本质上其还是变量,并没有变成常量,只是拥有了常量那不可变的性质。
3.2 const修饰指针变量
值得注意的是,const
修饰过变量虽然无法再通过变量名去访问修改该变量,但是可以另辟蹊径通过变量的地址去间接的访问,举例如下。
#include<stdio.h>
int main()
{const int a = 10;int* pa = &a;*pa = 100;printf("%d\n", a);return 0;
}
发现指针可以间接的修改const
修饰的变量,但问题来了,如果想让变量不被指针间接的修改掉,该这么做呢?答案是:使用const
修饰指向变量的指针。一般来说const
修饰指针变量,可以放在*
的左边,也可以放在*
的右边,意义是不一样的。
- const放在
*
的左边
const
限制的是*p
,意思是不能通过p来改变p指向对象的内容,但是p本身是可以改变的,能指向其他对象。举例如下。
#include<stdio.h>
int main()
{int a = 10;int const * p = &a;*p = 100;return 0;
}
#include<stdio.h>
int main()
{int a = 10;int b = 20;int const* p = &a;printf("p = &a = %p\n", p);p = &b;printf("p = &b = %p\n", p);return 0;
}
- const放在
*
的右边:
const
限制的是p
,意思是p本身无法改变,不能指向其他对象,但是可以通过p来改变指向对象的内容。举例如下。
#include<stdio.h>
int main()
{int a = 10;int b = 20;int* const p = &a;p = &b;return 0;
}
#include<stdio.h>
int main()
{int a = 10;int* const p = &a;*p = 100;printf("%d\n", a);return 0;
}
当然const
在指针变量的*
的两边都有时时,例如:const char* const p = &num
,这样既修饰的是*p
也修饰了p
。也就是说指针p既不能改变其所指向对象,也不能通过解引用的方法来改变内容。
4. 指针运算
指针的基本运算有三种:指针加减整数,指针减指针,指针的关系运算。其中指针加减整数已经讲述过了,不再多赘述。指针的关系运算顾名思义就是:指针跟指针之间的比大小,也就是地址之间的比大小。比较难理解的是指针减指针运算,那该怎么理解呢?如下所示。
首先,我们知道指针加减整数会得到一个新的指针,转换公式:指针减指针会得到一个正负整数,如上图所示。那么指针减指针得到的这个整数究竟代表什么?
答案: 元素的个数,指针与指针之间元素的个数。举个例子:通过指针减指针实现求字符串长度。
#include<stdio.h>
int my_strlen(char* str)
{char* head = str;char* end = str;while (*end != '\0'){end++;}return end - head;
}
int main()
{char str[] = "hello world";int sz = my_strlen(str);printf("字符串长度 = %d\n", sz);return 0;
}
指针减指针运算的前提是,两个指针的类型相同,并且指向同一块空间。指针减指针的绝对值,求的是两个指针之间的元素个数。
5. 野指针
野指针就是:指针指向的位置是不可知的、随机的、没有明确限制的。最为常见的产生野指针的情况有三种:指针未初始化,指针越界访问,指针指向的空间被释放。
5.1 指针未初始化
#include<stdio.h>
int main()
{int* p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;
}
为了避免这种情况出现,可以在创建一个暂时不需要使用的指针时,初始化该指针为NULL
(空指针)。NULL
是C语言中定义的一个标识符常量,值为0,地址为0x00000000。这个地址是无法访问的,读写该地址就会报错。
当然只初始化为空指针是不够的,在使用指针之前还是需要对其进行是否有效的判断,即判断是否不为NULL
,只有不为空才能进行读写操作。当后期不再使用某指针访问空间的时候,就应该把该指针置为NULL。举例如下。
#include<stdio.h>
int main()
{int* p = NULLif(p != NULL){...}return 0;
}
5.2 指针越界访问
#include<stdio.h>
int main()
{int arr[10] = {0};int* pa = arr;int i = 0;for(i = 0; i < 15; i++){*pa = i;pa++;//当指针指向的范围超出arr数组范围时,pa就是野指针}return 0;
}
指针越界访问是指:访问了不属于你的空间,也就是那些没有申请的空间。指针越界访问是无法避免的,程序员只能说尽量避免去写越界访问的程序。
5.3 指针指向的空间被释放
#include<stdio.h>
int* test()
{int n = 100;return &n;
}
int main()
{int* p = test();printf("%d\n", *p);
}
函数不能返回函数内部定义的局部变量的地址。
6. assert断言
assert(表达式)
是一个宏,常常被称为:断言,使用这个宏需要引用<assert.h>
头文件。如果表达式为真,assert
不会有任何动作;如果表达式为假,assert
就会果断报错终止运行程序。报错的信息会包含没有通过的表达式,以及包含这个表达式的文件名和行号。举例如下。
#include<assert.h>
#include<stdio.h>
int main()
{int a = 0;assert(a != 0);printf("%d\n", a);return 0;
}
assert
的使用对程序员是非常友好的,它不仅能自动标识文件地址和出问题的行号,还能做到无需更改代码,就能关闭assert
的机制。如果已经确定程序没有问题,不需要再做断言,只需在#include<assert.h>
语句前定义一个宏NDEBUG
,就能关闭所有断言。举例如下。
#define NDEBUG
#include<assert.h>
#include<stdio.h>
int main()
{int a = 0;assert(a != 0);printf("%d\n", a);return 0;
}
值得注意,在Release
版本中assert
直接就被优化掉了,不会执行。因为assert
的缺点是引入了额外的检查,增加了程序的运行时间,影响用户的体验。但在Debug
中assert
可以使用排查问题。
7. 传值调用和传址调用
7.1 传值调用
首先思考一个问题:如果想让函数一次返回2个参数该怎么做?return
能做到吗?做不到,因为return
返回参数是通过eax
寄存器返回的,只能做到返回一个参数。那该怎么办?能够通过函数的参数返回吗?如下所示。
#include<stdio.h>//交换两个参数里面的值
void exchange(int x, int y)
{int tmp = 0;tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;printf("a = %d, b = %d\n", a, b);exchange(a, b);printf("a = %d, b = %d\n", a, b);return 0;
}
当交换函数内的两个形参,函数外实参会发生变化吗?如上图所示。不会发生任何变化,那为什么不会改变呢?那是因为调用函数时,传递过去的形参和实参不是同一块内存空间,改变形参实参自然不会发生变化。如下所示。
结论: 函数传参时,形参只是实参的一份临时拷贝,形参发生变化,实参是不会跟着改变的,因为两者不在同一块空间。
7.2 传址调用
传值调用无法做到传递两个参数出来,那如果将传递的参数变为变量的地址呢?这样函数内部不就可以间接的访问到函数外部的参数了嘛。如下所示。
#include<stdio.h>
void exchange(int* pa, int* pb)
{int tmp = 0;tmp = *pa;*pa = *pb;*pb = tmp;
}int main()
{int a = 10;int b = 20;printf("a = %d, b = %d\n", a, b);exchange(&a, &b);printf("a = %d, b = %d\n", a, b);return 0;
}
可以看出,传址调用可以让主调函数和被调函数之间建立真正的联系,在被调函数内存可以修改主调函数中的变量。如下图所示。
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。
这篇关于你不知道的C语言知识(第四期:指针【1】)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!