一、内存和地址
在先了解指针之前,我们先来了解一下内存和地址的概念等知识。
1.内存
说白了,内存就是一片空间,用来存放东西的地方,那么对与计算机来讲,肯定是存放各种数据和程序的地方。
学习过机组知识后就知道,计算机上CPU(中央处器)在处理数据的时候,需要的数据是通过数据总线在内存中读取的,处理后的数据也会通过数据总线放回内存中(所以数据总线是双向的)。如图
但是CPU是怎么知道要进行数据的存入和读出,并且准确无误地找到数据要存放和读出的地方,这里就要引出地址的概念了。
2.地址
内存是一片很大的空间,把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节,这样可以更好地管理数据。然后给每一个内存单元都分配一个编号,可以通过这个编号找到对应的内存单元,也就是地址。而C语言中,我们可以这样理解,内存单元的编号 = 地址= 指针
所以通过地址(编号)我们就可以准确无误地找到数据要存放和读出的地方在哪里了,比如在内存那点的图片里,我们可以看到有很多地址线组成了地址总线,CPU会通过译码找到相应的地址编号,从而通过数据总线和控制总线来执行数据的存入还是数据的读出
计算机中常⻅的单位(补充):
⼀个⽐特位可以存储⼀个2进制的位1或者0
1Byte = 8bit(比特位)
1KB = 1024Byte(字节)
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
二、指针变量
首先我们应该知道在C语⾔中创建变量其实就是向内存申请空间,如下:
我们可以看到,当我们创建了整型变量a,就向内存中申请了4个字节的空间,一行代表一个字节,因为是以十六进制显示,10的十六进制为a,存入到内存中去了。
补充:创建的变量其实是给我们程序员看的,比如a它并没有存入到内存中去,没有什么实际意义,只是让我们自己知道,a代表着10,这样好进行其他操作(加减乘除等),所以以后大家在取变量名时要尽可能取有意义的名字,好让我们一眼就知道是关于什么的。
从图片中看到,我们是怎么拿出a的地址的?是用了&这个符号,接下来让我们来简单了解这个符号吧。
1.操作符(&)-取地址操作符
&a取出的是a所占4个字节中地址较小的字节的地址。虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。其中%p是用来打印地址的
2.指针变量
那我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要存储起来,方便后期再使用的,那我们把这样的地址值存放在哪里呢?答案是:指针变量中
int a = 10;
int * pa = &a;//其中a的地址就存放在了pa这个变量中,pa也就是存放了地址的变量,
//pa也叫做指针变量
3.指针类型
我们该如何理解指针的类型呢?其实很简单。
这⾥pa左边写的是 int * (int * 就是指针变量的类型), * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向是整型(int)类型的对象。
那如果有⼀个char类型的变量ch,ch的地址,要放在什么类型的指针变量中呢?
char ch = 'w';
pc = &ch;//pc 的类型怎么写呢?
相信你们也会了
char ch = 'w';
char * pc = &ch;
4.解引用操作符(*)
我们将地址保存起来,未来是要使用的,那怎么使用呢?
在现实生活中,我们使用地址要找到⼀个房间,在房间里可以拿去或者存放物品。
C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这里必须学习⼀个操作符叫解引用操作符(*)。
像这里,我通过指针变量pa间接来改变a的值,只是很有用的一步,现在你们可能觉得这是个多余的步骤,不过以后你们就不觉得了。
5.指针变量的大小
在前面我们已经介绍了地址有关的知识,我们应该知道了,CPU是通过地址总线到主存中读取数据或者存入数据的,平常所说的32位机器是指有32根地址线构成地址总线,即一个地址编号有32位比特,为4个字节;64位机器是指有64根地址线构成地址总线,即一个地址编号有64位比特,为8个字节。
- 32位机器:一个地址编号有4个字节,因为指针变量是用来存放地址的,所以指针变量的大小只需要4个字节即可存储地址。
- 64位机器:一个地址编号有8个字节,因为指针变量是用来存放地址的,所以指针变量的大小只需要8个字节即可存储地址。
上图是在32位机器下运行的结果,可以看到,无论指针变量的类型是什么,指针变量的大小都是相等的,而且只与机器的位数有关。看图片左上角X86(其实就是X32)
上图是在64位机器下运行的结果,看图片左上角X64。
结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
提问:不知道大家会不会对此指针变量类型产生疑惑?觉得指针变量类型它没有意义,因为指针变量的大小与指针变量类型无关呢?下面让我们进一步了解!!!
三、指针变量类型的意义
1.指针的解引用
例一:
通过F11逐条执行语句,当还未执行*pa=0之前,我们可以看到有一个4字节空间已经存入数据,然后pa指针变量指向了a,再解引用指针变量pa后,那片空间中的数据会被改变,即全部被改为0。
例二:
此时我们改变了pa指针变量的类型从int * 到char * ,通过F11逐条执行语句,当还未执行*pa=0之前,我们可以看到有一个4字节空间已经存入数据,然后pa指针变量指向了a,再解引用指针变量pa后,那片空间中的数据会被改变,但是我们发现只有其中的一个字节被改变为了0,并不是4个字节全部被改变。
注:&a的值可以放到char * 类型的指针变量中,因为不管是那种指针类型,大小都是与地址编号一样相等。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如: char * 的指针解引用就只能访问⼀个字节,而int * 的指针的解引用就能访问四个字节。
2.指针的运算(指针±整数)
我们可以看出, char * 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。也可以说指针变量类型决定了指针的步长(提示:理解这一点在后面理解起数组与指针结合的代码有大用处)
给一个更详细地对比:
int *pa;//pa+1类似等于pa+1*sizeof(int) 即指针偏移了4个字节; pa+n等于pa+n*sizeof(int)即偏移了4n个字节
char *pa;//pa+1类似等于pa+1*sizeof(char) 即指针偏移了1个字节; pa+n等于pa+n*sizeof(char)即偏移n个字节
3.void *指针
void * —>无具体类型的指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引用的运算。
int a = 10;
int *ptr1 = &a;//&a的类型是int * 与指针ptr1的类型int *一样,不会出现警告
char *ptr2 = &a;//&a的类型是int * 与指针ptr2的类型char *不一致,会出现警告,但是程序还是能够运行
//那么我们应该怎么才可以消除这个警告呢? 答:这就要用到void *无具体指针类型来接受各种指针类型
int a = 10;
void *ptr1 = &a;//此时就不会出现因为指针类型不符合而导致警告。
void *ptr2 = &a;
//但是会出现新的问题,相信有些同学已经想到了,你们还知不知道上面所讲的指针的步长
//步长就是指针+-整数时会偏移多少个字节。指针类型会导致解引用指针和指针+-整数时导致不同
int a =10;
void *ptr1 = &a;
void *ptr2 = &a;
*ptr1 = 10;//此时会报错,因为指针ptr1的类型是无具体的,根本不知道一次要访问多少个字节
*ptr2 = 0;//此时会报错,因为指针ptr2的类型是无具体的,根本不知道一次要访问多少个字节
ptr1++;//不知道指针类型,导致不知道+1应该偏移多少个字节
ptr2--;//不知道指针类型,导致不知道-1应该偏移多少个字节
那么 void* 类型的指针到底有什么⽤呢? ⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,
四、const修饰指针
1.const修饰变量
const修饰变量的时候,此时变量叫常变量。
这个被修饰的变量本质上还是变量,只是不能被修改,如上图中,想要修改num的值会出现报错。
那么我们怎么证明它本质还是变量?
如上图,变量n被const修饰,具有常属性,但是也不能在定义数组时来指定数组的大小,所以本质还是变量。
通过分析,我们明白了被const修饰的变量不能被改变,但是以我们现有的知识,我们应该是可以改变那个被修饰的变量,即通过取变量的地址(&),让指针间接地改变其值。如下图:
虽然这样确实可以更改其值,但是我们发现逻辑上就错误了,说不过去。我们来想想,我们用const来修饰变量,本意就是让变量不被改变,但是可以用指针来打破const的限制,这不是多此一举吗?那么我们也应该对指针做出一些限制,使其也不能改变const修饰的变量。这就引出了const修饰指针变量!!!
2.const修饰指针变量
⼀般来讲const修饰指针变量,可以放在的左边,也可以放在的右边,意义是不⼀样的。
int * p;//没有const修饰?
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰
例子一:无const修饰
例子一中,p是可以改变指向的,从一开始指向n到指向m,而且也可以修改指向的对象的值,n的值从10更改为了20,即指向和指向对象的值都可以改变
例子二:const放在 * 的左边
形式有二种,但意义相同:
- const int * p
- int const * p
const放在 * 的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,但是指针变量本身是可以改变的
例子三:const放在 * 的右边
例子四: * 两边都放const
代表即不可以修改指向的内容,也不可以修改指向的方向
五、指针运算
指针的基本运算有三种,分别是:
• 指针± 整数
• 指针-指针
• 指针的关系运算
1.指针±整数
在上面,我们已经讲了这点,此时该告诉你它有什么用。在指针和数组的结合代码中,我们可以通过指针来遍历整个数组,如下图所示:
为什么可以用指针来访问数组?关键的一点在于,定义数组时,内存中会开辟一片连续的空间给数组,即连续存放的
根据这节内容,指针±整数,让我们加深对指针类型的理解
如下图,我们也是用指针来遍历数组,但是这次我将int * p改为了 char * p,一样是把数组遍历出来了,那么下面的代码有没有错误?错在哪里了?
还是让我来直接告诉你们吧,看下图,内存中此时数据是这样存放的,p指针最开始是指向开头的(存放数据1) ,记得指针类型是会影响解引用时一次访问多少个字节,此时p指针的类型是char *,所以一次访问1个字节。导致访问了4个字节中的1个字节,遗落了其他3个字节,当数据够大时,会导致数据丢失。
让我们改变一下数组内数据的大小
此时打印出来的数据发生了错误
是不是觉得对指针又有一步更近地理解了
2.指针-指针
指针-指针的绝对值是指针和指针之间元素的个数
但是前提条件:二个指针指向同一个空间
答案是9
应用:
例子一:我们通过调用strlen()函数来计算字符数组中字符的个数
例子二:我们通过自己写一个函数来计算字符数组的个数(运用指针±整数)
#include<stdio.h>
#include<string.h>
size_t my_strlen(char* p)//传的数据类型是什么,则接受的数据类型就是什么
{
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],arr的指针类型是char *
printf("%zd\n", len); //结果是6
return 0;
}
例子三:我们通过自己写一个函数来计算字符数组的个数(运用指针-指针)
size_t my_strlen(char* p)
{
char* start = p;
char* end = p;
while (*end != '\0')
{
end++;
}
return end - start;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);//数组名是首元素地址,即arr == &arr[0],arr的指针类型是char *
printf("%zd\n", len);//结果也是6
return 0;
}
3.指针的关系运算
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
while (p <= &arr[sz - 1])//数组中的元素从低地址到高地址存放,当p小于arr[sz-1]时,代表还未遍历完数组元素
{
printf("%d ", *p); //结果为1 2 3 4 5 6 7 8 9 10
p++;
}
return 0;
}
6.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1野指针的成因
1. 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值,即p的指向不确定,会改变
*p = 20;//解引用操作符就会形成非法访问,此时p就是野指针
return 0;
}
2.指针越界访问
#include <stdio.h>
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;
}
当p指向下标为10的元素时,这时进行解引用操作,会导致非法越界访问,此时p就是野指针
3. 指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
重点要知道,局部变量n出test()函数会被销毁,内存空间会返回操作系统管理,此时再通过p指向的空间进行访问(解引用),会导致非法访问
6.2规避野指针
6.2.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址
会报错。
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL; //不知道p2指向哪里,就先给p2指向NULL
return 0;
}
6.2.2 小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。
6.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
for(i=0; i<10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使⽤的时候,判断p不为NULL的时候再使⽤
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //指针使用之前检查有效性
{
//...
}
return 0;
}
6.2.4 避免返回局部变量的地址
不要返回局部变量的地址
7.assert 断言
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断言”。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生
任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写入⼀条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
//代码一
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 0;
int* p = &a;
assert(p != NULL);
return 0; //程序不会报错
}
//代码二
#include<stdio.h>
#include<assert.h>
int main()
{
int a = 0;
int* p = NULL;
assert(p != NULL);
return 0;
}
代码二中,不满足条件会报错,并给出在哪一行错误
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和
出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问
题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include <assert.h>
8.指针的使用和传址调用
8.1 strlen的模拟实现
size_t strlen ( const char * str );//strlen函数原型
#include<stdio.h>
#include<assert.h>
size_t my_strlen(const char* s)//const限制*s对其字符串数组进行修改
{
size_t count = 0;
assert(s != NULL);//检测指针s是否有效
while (*s)
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
printf("%zd", my_strlen(arr));
return 0;
}
8.2传值调用和传址调用
8.2.1传值调用
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);//输入 3 5
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);//结果 3 5
return 0;
}
其中x,y的地址与a,b不一样,x,y是形参,a,b是实参;形参是实参的一份临时拷贝,即对形参修改值也不会对实参有影响
8.2.2传址调用
#include <stdio.h>
void Swap2(int*px, int*py)//用指针接收a,b的地址,则可以通过px,py指针分别找到a,b的空间,再通过解引用操作修改a,b的值
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。
谢谢观看!希望以上内容帮助到了你,对你起到作用的话,可以一键三连!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!