C语言:指针(第一天)
预备知识
内存地址
- 字节:字节是内存的容量单位,英文名byte,一个字节有8位,即1byte=8bits
- 地址:系统为了便于区分每一个字节而对他们逐一进行的编号,称为内存地址,简称地址。
基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址(首地址)。
取址符
- 每个变量都是一块内存,都可以通过取址符
&
获取其地址。 - 例如:
c
int a = 100;
printf("整型变量a的地址是:%p\n",&a);
int c = 'x';
printf("字符变量c的地址是:%p\n",&c);// 地址采用12位16进制数表示
- 注意:
- 虽然不同变量的尺寸是不同的,但是他们地址的尺寸是一致的。
- 不同的地址虽然形式上看起来是一样的,但是由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
为什么要引入指针
- 为函数修改实参提供支持。
- 为动态内存管理提供支持。
- 为动态数据结构(链表,队列等)提供支持。
- 为内存访问提供另一种途径。
变量指针与指针变量
指针概述
- 内存地址:系统为了内存管理的方便,将内存划分为一个个的内存的单元(通常1个字节),并为每一个内存单元进行编号。内存单元的编号称之为该内单元的地址。一般情况下,我们每一个变量都是由多存单元构成的,多以每个变量的内存地址,其实就是这个变量对应的第一个内存单元的地址,也叫基地址/首地址。
- 变量指针 :变量地址称之为改变了的指针(本质是地址)。变量地址往往是指变量在内存中的第一个内单元的编号(首地址)。
c
int a;
&a --- 变量a的地址,也称为变量a的指针
int arr[2];
&arr --- 数组arr的地址,也称为数组arr的指针
指针就是地址,地址就是指针。
-
指针变量 :专门存放指针的变量(本质是变量),简单来说,用来存放地址的变量就是指针变量。
-
指向 :指针变量中存放
谁
的地址,就说明该指针变量指向了谁
。 -
指针的尺寸:
- 指针的尺寸指的是指针所占内存的字节数。
- 指针所占内存,取决于地址的长度,而地址的长度则取决于系统的寻址范围,即字长。
- 结论:指针尺寸只跟系统的字长有关系,跟具体的指针的类型无关。
小贴士:
Linux系统中打印地址时,最多显示12个十六进制数,为什么?
Linux64位操作系统中,一个内存地址占8个字节,一个字节8bit位,所以一个地址88=64bit位,每4个bit可以表示1个十六进制数;64个bit位用十六进制表示最多有16个数值位;
系统为了寻址方便,默认当前计算机系统没必要寻址64bit为,只寻址了48个bit为,所以用12个十六进制数表示一个地址。
二进制:01001010 十六进制:0x4A 416+10=74
注意:==在Limnux64位操作系统中,指针类型的变量占8个字节的内存空间,==在LInux32位操作系统中,指针类型的变量占4个字节的内存空间。
-
在C语言中对内存数据(如变量、数组元素等)的存取有两种方式:
-
直接存取:
- 通过基本类型(整型、浮点型、字符型)的变量,访问这个变量代表的内存空间的数据
- 通过数组元素的引用,访问这个引用代表的内存空间的数据。
cint = 10;// 存 printf("%d",a);// 取 int arr[] = {11,22,33};// 存 arr[0] = 66;// 存 printf("%d",arr[0]);// 取
-
简接存取:
- 通过指针变量,间接的访问内存中的数据。
*
:读作解引用,是解引用符号。如果*
前有数据类型,读作指针
c#include <stdio.h> int main(int argc, char *argv[]) { // 定义一个普通变量 int i = 3; // 定义一个指针变量,并赋值 int *i_point = &i;// 指针变量的数据类型要和存储的地址变量类型一致 // 访问普通变量(直接访问) printf("直接访问-%d\n",i); // 访问指针(地址访问)%p 访问地址 printf("地址访问-%p,%p\n",&i,i_point); // 访问指针变量(简介访问)解引用:通过这个指针变量存储的地址,访问这个地址对应空间的数据 printf("间接访问-%d\n",*i_point);// 3 }
-
指针变量的定义
语法:
c
数据类型 *变量列表;
举例:
c
int a;// 普通变量,拥有真实的数据存储空间
int *a_,*b_;// 指针变量,无法存储数据,只能存储其他变量的地址
注意:
-
虽然定义指针变量
*a
,是在变量名前加上*
,但是实际变量名依然为a
,而不是*a
。 -
使用指针变量间接访问内存数据时,指针变量必须要有明确的指向。
-
如果想要借助指针变量间接访问指针变量保存的内存地址上的数据,可以使用指针变量前加
*
来间接访问;指针变量前加
*
,我们称之为对指针变量解引用
。cint i = 5, *p; p = &i;// 将i的地址赋值给指针变量p printf("%x,%p\n",p,p);// 两个都是打印地址,%x打印的地址不带0x,%p打印的地址带0x printf("%d\n",*p);// 间接访问i的值,也称为解引用p对应地址空间的值 *p = 10;// p是访问地址,*p是访问地址空间对应的数据
-
指针变量只能指向同类型的变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型。
-
指针变量在定义时可以初始化:这一点和普通变量是一样的。
cint i = 5; int *p = &i;// 将i的地址赋值给指针变量p printf("%d\n",*p);
指针变量的使用
使用
-
指针变量的赋值
c// 方式一: int a,*p; p = &a; // 指针变量的值是其他变量的地址 // 方式二: int a,*p,*q = &a; p = q;
-
操作指针变量的值
cint a,*p,*q = &a; p = q; printf("%p",p);// 此时返回的是变量a的地址空间
-
操作指针变量指向的值
cint a = 6,*q = &a;b = 25; *q = 10; printf("%d,%d",*q,a);// 10,10 q = &b; printf("%D,%d",*q,a);// 25,10
两个有关运算符的使用
&
取地址运算符。&a是变量a的地址。*
指针运算符(或称之为"间接访问"运算符,解引用符),*p是指针变量p指向的对象的值。
案例1:
需求:通过指针变量访问整型变量
代码:
c
#include <stdio.h>
void main()
{
int a = 3,b = 4,*pointer_1 = &a,*pointer_2 = &b;
printf("a=%d,b=%d\n",*pointer_1,*pointer_2);
}
案例2
需求:声明a,b两个普通变量,使用间接存取的方式实现数据的交换。
代码:
c
/**
* 需求:声明a,b两个普通变量,使用间接存取的方式实现数据的交换。
*/
#include <stdio.h>
void main()
{
int a = 3, b = 4, *p_a, *p_b, *p_t;
printf("%d,%d\n",*p_a,*p_b);// 3,4
// 交换位置
p_t = p_a;
p_a = p_b;
p_b = p_t;
printf("%d,%d\n",*p_a,*p_b);// 4,3
}
案例3
指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
代码:地址交换
c
/**
* 需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
*/
#include <stdio.h>
void main()
{
int a = 3, b = 5, *p_a = &a, *p_b = &b, *p_t;
if (a < b)
{
p_t = p_a; // 操作指针变量,不会影响到数据本身
p_a = p_b;
p_b = p_t;
}
printf("按从大到小输出a,b的值:%d > %d\n", *p_a, *p_b);
}
代码:数据交换,不推荐
c
/**
*需求:指针变量应用。输入a、b两个整数,按先大后小的顺序输出a和b。
*/
#include <stdio.h>
void main()
{
int a = 3, b = 5, *p_a = &a, *p_b = &b, *p_t;
if (a < b)
{
*p_t = *p_a; // 操作指针地址指向的内存空间,也就是直接操作变量a
*p_a = *p_b;
*p_b = *p_t;
}
printf("按从大到小输出a,b的值:%d > %d\n", *p_a, *p_b);
}
指针变量做函数参数
指针变量做函数参数往往传递的是变量的地址(首地址)借助于指针变量间接访问是可以修改实参变量数据的。
案例1
需求:要求函数处理,用指针变量做函数的参数
-
方式1:交换指向(指向的普通变量的值不变)
c#include <stdio.h> // 自定义一个函数,实现两个数的比较 void swap(int *p_a,int *P_b) { int *P_t; // 这种写法只会改变指向,并不会改变地址对应空间的数据 p_t = p_a; p_a = p_b; p_b = p_t; printf("%d > %d\n",*p_a,*p_b); } void main() { int a = 3, b = 5; // int *p_a = &a, *p_b = &b; // if(a < b) swap(p_a,p_b); if(a < b) swap(&a,&b); else printf("%d > %d\n",a,b); }
-
方式2:交换数据(指向普通变量的值改变)
c#include <stdio.h> // 自定义一个函数,实现两个数的比较 void swap(int *p_a,int *P_b) { int t; // 这种写法,改变的是指针指向地址空间的数据 t = *p_a; *p_a = *p_b; *p_b = t; printf("%d > %d\n",*p_a,*p_b); } void main() { int a = 3, b = 5; // int *p_a = &a, *p_b = &b; // if(a < b) swap(p_a,p_b); if(a < b) swap(&a,&b); else printf("%d > %d\n",a,b); }
指针变量指向数组
数组元素的指针
-
数组的指针就是数组中第一个元素的地址,也就是数组的首地址。
-
数组的元素的指针是指数组元素的首地址。因此,同样可以用指针变量来指向数组或数组元素。
-
在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也是指针。
c#include <stdio.h> int main() { // 定义一个普通数组 int a[] = {11,22,33}; // int a[]可以看作匿名创建了三个连续空间的int变量 // 使用指针变量存储数组的第一个元素的首地址,也就是数组的首地址 int *p1 = &a[0]; // 数组的首地址 int *p2 = a; // 在我们C语言中,由于数组名代表数组的首地址,因此,数组名实际上也就是指针 printf("%p,%p,%p", p1, p2, a); return 0; }
注意:虽然我们定义了一个指针变量接收了数组地址,但不能理解为指针变量指向了数组,而应该理解为指向了数组的元素。
指针的运算
指针变量必须要指向数组的某个元素。
序号 | 指针运算 | 说明 |
---|---|---|
1 | 自增:p++、++p、p=p+1 | p+=1 | 让指针变量指向下一个元素 |
2 | 自减:p--、--p、p-=1 | 让指针变量指向上一个元素 |
3 | 加1个数:p+1 | 下一个元素的(首)地址 |
4 | 减1个数:p-1 | 上一个元素的(首)地址 |
5 | 指针相减:p1-p2 | p1,p2之间相差几个元素 |
6 | 指针比较:p1<p2 | 前面的指针小于后面的指针 |
说明:
① 如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的 下一个元素,p-1指向同一数组中的上一个元素。即p+1或p-1也表示地址。但要注意的是,虽然指针变量p中存放的是地址,但p+1并不表示该地址加1,而表示在原地址的基础上加了该数据类型所占的字节数d。
② 如果p原来指向a[0],执行++p后p的值改变了,在p的原值基础上加d,这样p就指向数组的下一个元素a[1]。d是数组元素占的字节数。
③ 如果p的初值为&a[0]则p+i 和a+i 就是数组元素a[i]的地址,或者说,它们指向a数组的第 i个元素 。
④ (p+i) 或(a+i)是p+i或a+i所指向的数组元素,即a[i]。
⑤ 如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是两个地址之差除以数组元素的长度d。
c
#include <stdio.h>
int main(int argc, char *argv[])
{
int arr[] = {11, 22, 33, 44, 55};
int *p1 = arr + 4;
int *p2 = arr + 1;
printf("%ld\n", p1 - p2); // 3
return 0;
}
案例1:
通过下标法和指针法遍历数组
c
#include <stdio.h>
int arr1()
{
// 定义一个普通数组
int arr[] = {11,22,33,44,55};
// 计算数组的大小
int len = sizeof(arr) / sizeof(arr[0]);
//创建指针变量
int *p = arr;
// 创建循环变量
register int i = 0;// 将我们的变量存放在寄存器中,提高执行效率
// 遍历
for(;i < len;i++)
{
printf("[1] %d ",arr[i]);// 下标法,这种写法可读可写
printf("[2] %d ",*(arr+i));// 指针法,这种写法需要注意,arr+i无法修改数组,只读
// printf("[3] %d ",*(p+i));// 指针法,这种更为灵活,可读可写,建议
printf("[3] %d ",*p);// p 获取当前p指向的数组元素的地址
p++;
printf("\n");
}
}
int main(int argc,char *argv[])
{
arr1();
return 0;
}
案例2:
需求:推到一下代码的运行结果
c
#include <stdio.h>
int arr2()
{
int arr[] = {11,22,33,44,55,66,77,88};
int *p = arr;// *p取出来的数据是11
printf("%d\n",*p);// 11
p++;// 指针+1,元素的值不变,*p取出来的元素是22
printf("%d\n",*p);// 22
int x = *p++;// x = 22,p++指向下一个元素空间 → 33,*p = 33;++ 的运算级高于*
printf("%d,%d\n",x,*p);// 22,33
int y = *(++p);// y = 44,++p 指向下一个元素空间 = 44,*(++p) = 44
printf("%d,%d\n",y,*p);// 44,44
(*p)++;// 先解引用,取出p对应地址空间的值,然后再给其++,45+1=45
printf("%d\n",*p);
}
int arr1()
{
// 定义一个普通数组
int arr[] = {11,22,33,44,55};
// 计算数组的大小
int len = sizeof(arr) / sizeof(arr[0]);
//创建指针变量
int *p = arr;
// 创建循环变量
register int i = 0;// 将我们的变量存放在寄存器中,提高执行效率
// 遍历
for(;i < len;i++)
{
printf("[1] %d ",arr[i]);// 下标法,这种写法可读可写
printf("[2] %d ",*(arr+i));// 指针法,这里的只读,指的是arr不能再冲新赋值
// printf("[3] %d ",*(p+i));// 指针法,这种更为灵活,可读可写,建议,这里的p是可以重新赋值的
printf("[3] %d ",*p);// p 获取当前p指向的数组元素的地址
p++;
printf("\n");
}
}
int main(int argc,char *argv[])
{
arr2();
return 0;
}