【C语言系列】深入理解指针(1)
前言
总所周知,C语言中指针部分是非常重要的,这一件我们会介绍指针相关的内容,当然后续我还会出大概4篇与指针相关的文章,来深入的讲解C语言指针部分,希望能够帮助到指针部分薄弱或者根本不会的程序员们,后续文章尽情期待!
一、内存和地址
1.1内存
电脑上有内存,那么我们就会想内存是怎样高效管理的呢?
其实,就是把内存分为一个一个的内存单元,每个内存单元的大小为1个字节。
其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个比特位,就像同学们住的八人间,每个人是一个比特位。
每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间。
计算机中常见的单位(补充):
计算机在识别、存储、运算的时候都是使用的2进制;⼀个比特位可以存储⼀个2进制的位1或者0。
1byte(字节) = 8bit(比特位)
1KB = 1024byte(字节)(或者用2^10 =1024byte(字节)表示)
1MB= 1024KB(或者用2^10 =1024KB表示)
1GB = 1024MB(或者用2^10 =1024MB表示)
1TB = 1024GB(或者用2^10 =1024GB表示)
1PB = 1024TB(或者用2^10 =1024TB表示)
在计算机中我们把内存单元的编号称为地址,C语言中给地址起了新的名字叫:指针,即指针的本质就是地址,也就是内存单元的编号。
1.2究竟该如何理解编址
今天我们重点关注一下地址总线和数据总线,通过图可以看出,数据是从CPU输入(写)到内存中,然后由内存输出(读)到CPU上,CPU是通过地址总线来发送目标地址,访问内存位置;而内存是通过数据总线把数据传输给CPU的。
这张图我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0/1(即电脉冲的有无),那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
总结:1、内存会被划分为一个一个的内存单元,每个内存单元的大小是1个字节。
2、每个内存单元都会给一个编号 == 地址 ==C语言中也叫指针。
二、指针变量和地址
2.1取地址操作符(&)
下面我们观察一段代码,并进行调试:
int main()
{
int a = 25;//变量创建的本质是什么呢?是在内存中开辟一块空间
//int占4个字节,&a ——>只取第1个字节的地址(即4个地址中最小的那个地址)
&a;//& —— 取地址操作符
printf("%p\n", &a);//%p —— 是专门用来打印地址的 —— 其实是以16进制的形式打印的
return 0;
}
运行结果如下图:
调试后发现a的地址为:0x00000090BB6FFAE4,并且以16进制形式打印出来(25转换为16进制的话就是19),当然编译器每次分配的地址可能是不一样的,可能打印的结果和地址会有差别,这是正常现象。
2.2指针变量和解引用操作符(*)
2.2.1指针变量
那我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x00000090BB6FFAE4,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这个地址值存放在哪里呢?存放在指针变量。
如下图所示:
#include <stdio.h>
int main()
{
int a = 10;
int * p = &a;//取出a的地址,并存储到指针变量p中。
//地址 —— 指针
//p是指针变量 —— 存放指针的变量
//即指针变量是用来存放地址的变量
return 0;
}
注:指针变量也是一种变量,这种变量就是用来存放地址(指针)的,存放在指针变量中的值都会被理解为地址。
2.2.2如何拆解指针类型
int a = 20;
int *pa = &a;
结合上述代码我们可以看到,pa的类型是int*,那如何理解pa的类型呢?
这里pa左边写的是int* ,*是在说明pa是指针变量,而前面的int 是在说明pa指向的是整型(int)。
2.2.3解引用操作符
我们将地址保存起来,未来是要使用的,那怎么使用呢?
C语⾔中其实也是一样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里使用的操作符叫解引用操作符(*)。
#include <stdio.h>
int main()
{
int a = 20;
int*p = &a;
*p = 100;
//解引用操作(间接访问操作符)*p等于a
//* —— 解引用操作符
printf("%d\n",a);
return 0;
}
*上面代码中第6行就使用了解引用操作符,p 的意思就是通过pa中存放的地址,找到指向的空间,p其实就是a变量了;所以p = 100,这个操作符是把a改成了100。
这里很容易理解错误,很多初学者认为这里把 a = 100;不就可以了吗?为什么要用指针呢?那是不是就没必要学指针呢?
答案当让是否认的,其实这里是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,写代码就会更加灵活,相信后期初学者就会慢慢理解了。
总结:1、指针其实就是地址;
2、指针变量是存放指针(地址)的。
一般口语中说的指针一般都是指:指针变量比如:int *p; 。
2.3指针变量的大小
前面我们了解过,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,那么一个地址就是32个bit位,需要4个字节才能存储,那么64位机器也一样,就需要8个字节来存储。
如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
像上述指针变量p,是需要向内存申请一块空间的,这样才有能力存放地址。
那么指针变量的大小是多少呢?
指针变量是需要多大空间,是取决于存放的是什么?存放的是地址,地址的存放需要多大空间,指针变量的大小就是多大。
32位机器上(X86):地址是32个0/1的二进制序列,存储起来需要32个bit位,也就是4个字节,指针变量的大小就是4个字节。
64位机器上(X64):地址是64个0/1的二进制序列,存储起来需要64个bit位,也就是8个字节,指针变量的大小就是8个字节。
注:指针变量的大小与类型无关!只要指针类型的变量,在相同的平台下,大小都是相同的。
三、指针变量类型的意义
3.1指针的解引用
接下来我们看下面两个代码:
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;//00000000
return 0;
}
int main()
{
int a = 0x11223344;
char* pa = &a;
*pa = 0;//00332211
return 0;
}
观察上述代码得出结论:指针类型决定了指针进行解引用操作的时候访问多大空间。
int * 的指针解引用访问4个字节。
char *的指针解引用访问1个字节。
指针的类型决定了,对指针解引用的时候有多大的权限(依次能操作几个字节)。
3.2指针±整数
下面我们看这样一个代码:
#include <stdio.h>
int main()
{
int a = 10;
int*pa = &a;
char*pc = &a;
printf("pa = %p\n",pa);//00000043DC1FFC94
printf("pa + 1 = %p\n",pa + 1);//00000043DC1FFC98
printf("pc = %p\n",pc);//00000043DC1FFC94
printf("pc + 1 = %p\n",pc + 1);//00000043DC1FFC95
return 0;
}
运行结果如下图:
结论:指针类型决定了指针的步长,就是向前/向后走一步走多大距离。
type* p;
p + i是跳过i个type类型的数据,相当于跳过了i*size(type)个字节。
int*p;
p + 2相当于跳过2个int类型的数据,相当于跳过了2*size(int) = 8个字节。
根据实际的需要,选择适当的指针类型才能达到效果。
3.3void*指针
void指针可以理解为无具体类型的指针(或者叫泛型指针),可以用来接受任意类型的地址。但也有局限性,void类型的指针不能直接进行指针的±整数和解引用的运算。
我们观察下面的代码:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
运行时会报错如下图所示:
这里我们观察后不难看出,void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
void*类型的指针到底有什么作用呢?
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。
四、const修饰指针
4.1const修饰变量
变量是可以被修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量,但是如果我们希望⼀个变量不能被修改,那么我们就可以使用const修饰这个指针变量,这就是const的作用。
如下面代码所示:
#include <stdio.h>
int main()
{//const是常属性 —— 不能被修改了
const int n =10;//n是变量
n = 0;//这里会报错
//const修饰了n之后,n不能被修改了,但是n还是变量。
printf("%d\n",n);//C++中const修饰的n就是常量。
return 0;
}
上述代码中n是不能被修改的,其实n本质还是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n进行修改,就不符合语法规则,就会报错,致使无法直接修改n。
#include <stdio.h>
int main()
{
const int n = 10;
//n = 0;//err
int*p = &n;
*p = 20;
printf("n = %d\n",n);
return 0;
}
运行结果如下图:
这里直接给n的地址是可以修改n中的内容的,但是这打破了语法规则。
4.2const修饰指针变量
预备知识如下图:
⼀般来说const修饰指针变量,可以放在* 的左边,也可以放在* 的右边,意义是不⼀样的。
//两种情况
int * p;//没有const修饰
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰
#include <stdio.h>
//const放在*的左边情况
void test1()
{
const int n = 10;
int m = 100;
const int* p = &n;
//*p = 20;//err
p = &m; //ok
}
//const放在*的右边情况
void test2()
{
const int n = 10;
int m = 100;
int * const p = &n;
//*p = 20; //0k
p = &m; //err
printf("%d\n",n);
}
int main()
{
//测试const放在*的左边情况
test1();
//测试const放在*的右边情况
test2();
return 0;
}
const修饰指针变量有2种情况:
1、const放在*的左边:限制的是 *p,意思是不能通过p来改变p指向的对象的内容,但p本身是可以改变的,p可以指向其他对象。
2、const放在 *的右边:限制的是p,意思是不能修改p本身的值,但是p指向的内容是可以通过p来改变的。
五、指针运算
指针的基本运算有三种,分别是:
指针±整数
指针-指针
指针的关系运算
5.1指针±整数
数组在内存中是连续存放的,随着数组下标的增长,地址是由低到高变化的。
#include <stdio.h>
int main()
{
int arr[0] = {1,2,3,4,5,6,7,8,9,10};
//打印数组内容
//下标:0 1 2 3 4 5 6 7 8 9
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
int*p = &arr[0];
for(i = 0;i < sz;i++)
{
printf("%d",*p);
p++;//p = p + 1;
//这里可以合起来写为:printf("%d",*(p+i));这里的p+i就是指针+整数
}
return 0;
}
5.2指针-指针
指针-指针,运算的前提是两个指针指向了同一块空间,指针-指针得到是指针和指针之间元素的个数。
指针-指针就好比日期-日期 ==天数,但是日期+日期就什么也不是了,所以也没有指针+指针。
#include <stdio.h>
int main()
{//指针-指针,运算的前提是两个指针指向了同一块空间
int arr[10] = {0};
printf("%d\n",&arr[9] - &arr[0]);//指针-指针 9
//&arr[0] - &arr[9];//9
return 0;
//指针-指针的绝对值是指针和指针之间的元素个数
}
strlen是库函数,是专门用来求字符串长度的,接下来我们用指针-指针来实现这个库函数,代码如下:
#include <stdio.h>
int my_strlen(char*p)
{
char*p1 = p;
while(*p != '\0')
{
p++;
}
return p - p1;
}
int main()
{
char arr[] = "abcdef";
//数组名其实就是数组首元素的地址
//arr == &arr[0];
int len = my_strlen(arr);
printf("%d\n",len);
return 0;
}
5.3指针的关系运算
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int sz =sizeof(arr)/sizeof(arr[0]);
int*p = &arr[0];
while(p < arr + sz)//指针的关系运算(即指针的大小比较)
{
printf("%d",*p);
p++;
}
return 0;
}
六、野指针(很危险)
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
6.1野指针的成因
1.指针变量未初始化
#include <stdio.h>
int main()
{
int*p;//局部变量
//局部变量未初始化的时候,它的值是随机值0xcccccccc
*p = 20;
printf("%d\n",*p);
return 0;
}
2.指针越界访问
int main()
{
int arr[10] = {0};
int*p = &arr[0];
int i = 0;
for(i = 0;i <= 11;i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针。
*(p++) = i;
}
return 0;
}
3.指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n",*p);
return 0;
}
6.2如何规避野指针
6.2.1指针初始化
如果明确知道指针的指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。NULL 是C语言中定义的一个标识符常量其值为0,0也是地址,这个地址是无法使用的,读写该地址会报错。
#include <stdio.h>
int main()
{
int a = 10;
int*pa = &a;
int*p = NULL;//NULL的值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
return 0;
}
int*p = NULL;
*p = 20;//这里会报错的
6.2.2小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。
6.2.3指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问,
同时使用指针之前可以判断指针是否为NULL。
6.2.4避免返回局部变量的地址
如造成野指针的第3个例子,不要返回局部变量的地址。
七、assert断言
assert.h 头文件定义了宏assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。(使用assert时,要包含头文件<assert.h>)。
assert(p != NULL);//验证变量p是否等于NULL。
上面代码在程序运行到这一行语句时,验证变量p是否等于NULL 。如果确实不等于继续运行,否则就会终止运行,并且给出报错信息提示。
assert()宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assert()就会报错,在标准错误流stderr中写入⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
#include <stdio.h>
#include <assert.h>
int main()
{
int a = 0;
int*p = NULL;
assert(p != NULL);//err
*p = 20;
printf("%d\n",a);
return 0;
}
assert()有几个好处:它不仅能自动标识文件和出现问题的行数,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要做断言,就可:
#define NDEBUG
#include <assert.h>
一般在Debug中使用,在Release版本中会被优化掉。
assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。
八、指针的使用和传值使用
8.1strlen的模拟实现
库函数strlen的功能是求字符串长度,统计\0之前的字符的个数。
函数原型如下:
size_t strlen ( const char * str );
参数str接收一个字符串的起始地址,然后开始统计字符串中\0 之前的字符个数,最终返回长度。如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0计数器就+1,直到遇到\0为止。
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char*s)
{
size_t count = 0;
assert(s != NULL);
while(*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
//strlen() —— 求字符串的长度 —— 统计的是字符串中\0之前的字符的个数。
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n",len);
return 0;
}
8.2传值调用和传址调用
题目:写一个函数,交换两个整型变量的值。
#include <stdio.h>
void Swap(int x,int y)//传值调用
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n",a,b);//a = 10, b = 20
Swap(a,b);
printf("交换后:a = %d b = %d\n",a,b);//a = 10, b = 20
return 0;
}
运行结果如下图:
结论:当实参传递给形参的时候,形参是有自己独立的空间的,形参是实参的一份临时拷贝,对于形参的修改,不会影响实参。
#include <stdio.h>
void Swap2(int*pa,int*pb)//传址调用
{
int z = 0;
z = *pa;
*pa = *pb;
*pb = z;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n",a,b);//a = 10, b = 20
Swap2(&a,&b);
printf("交换后:a = %d b = %d\n",a,b);//a = 20, b = 10
return 0;
}
运行结果如下图:
总结:只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。
原文地址:https://blog.csdn.net/2301_80179750/article/details/145226988
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!