文章目录
一、基础内容与提醒
1、基础内容
内存地址、首地址、存储空间大小
要想学好指针,我们首先要明白什么是内存地址。
我们知道计算机通过晶体管的开关状态来记录数据。它们以八个为一组,我们称之为字节。计算机上有许多组这样的晶体管,我们将每组晶体管,即每个字节看作成一个“房间”,每个房间则有八个二进制位。
每个”房间“从零开始,都有对应的房号,这些房号就是我们所说的内存地址。
拿 int 数据类型举例,假设其房号为301,因为它占据了四个字节,所以它要住四个房间,即
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rRA5JbvS-1666702537859)(https://gitee.com/Kivykiwi/66666/raw/master/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20221022161048.jpg)]
我们把该数据对象的第一个房间号称作其首地址,它需要的房间数量即为存储空间大小。
这也告诉我们,记录数据对象在内存中的位置时,需要两个信息:
a.数据的首地址
b.数据的存储空间大小
2、提醒
为了能让指针这一内容学起来更加轻松,现在请牢记数据类型这一个关键词,后续内容的分析离不开对数据类型的清晰认识。
例如:
int a 的数据类型为 int ;
int b[10] 的数据类型为 int[10] 。
二、指针
1.取地址运算符&
作用:写在数据对象左边,可以获取数据对象的首地址和所需存储空间大小。
例子:
int n;
类型 pn = &n;
则变量pn储存了n的首地址与其所需的空间大小,那么变量pn的类型又是什么呢?先留个悬念。
2.声明指针类型的变量与取值运算符*
指针的定义:设一个数据对象为 x ,另一个数据对象为 p 。p 存储了 x 的首地址和所占空间大小。那么, p 称之为 x 的指针,或者是 p 指向 x 。
声明指针的公式:
目标类型 * 变量名
如 int * pn
因此,我们可以知道,上述的变量pn 的类型为 int * 。
取值运算符*:将 * 写在一个指针的左边吧,可以根据指针中存储的首地址和空间大小找到目标数据对象。
接下来看下列代码:
#include <stdio.h>
int main()
{
int n=19;
int* pn = &n;
printf("&n=%u",&n);
printf("pn=%u",pn);
printf("*pn=%d",*pn);
printf("sizeof pn = %d",sizeof(pn));
return 0;
}
结果:&n=6684180; pn=6684180; *pn=19 sizeof(pn)=8
下列是知识点的灌输:
a. pn 的值,就是目标数据对象的首地址,即 n 的首地址,而目标数据对象 n 所需空间大小则与指针类型有关,类型 int* 标记变量 n 占用 sizeof(int) 字节,即4个字节。
b. *pn 的值,即根据 pn 中的首地址与大小找到的数据对象的值,即 n 的值。
c. %u则用于输出目标数据对象的地址。
d. 指针类型的大小,即 sizeof(pn) 的值,有编译器或编译配置决定,指针类型在32位程序中占4字节,在64位程序中占8字节。
3.强制转换指针类型
指针类型可以强制转换,转换后,其首地址与存储空间大小也将发生改变
int n = 123;
int* pn = &n;
char* pc = (char*)pn;
printf("pc=%u,pn=%u",pc,pn);
printf("*pc=%d,*pn=%d",*pc, *pn);
结果:pc=6684180,pn=6684180; *pc=123, *pn=123;
由上述结果得,pn的类型由 int* 转换为 char* ,赋值后,pn 与 pc 均存储了 n 的首地址,均打印了同样的地址,均打印了 n 的值,所以强制转换成功。
注:上述转换是将 int* 转换为 char* , pc 的存储空间大小为1个字节,因此,如果n 的值大于127,如1234,由于 *pn 的数据类型为4个字节的 int , *pn 的值也为1234,但数据类型大小为1个字节的 *pc 的值则会变为没有意义与逻辑的数字。
4.通过指针修改目标数据对象变量的值
假设变量 n 的值为100 ,存储在了6684180的这个地址上,其指针为 pn , 则 *pn 的值也为100 ,但当我们将 *pn 的值修改为1000时,变量 n 的值也会变为1000 ;
int n = 100;
int* pn = &n;
printf("n=%d",n);
*pn = 1000;
printf("n=%d",n);
结果:n=100; n=1000
即通过指针可以修改目标数据对象变量的值。
三、指针运算
1.指针的赋值
由于指针包含了数据对象的首地址与空间大小这两种信息,因此,我们在对指针赋值的时候不能只用数字进行赋值,如:int* pn = 100;这样是错误的,我们因该将要赋予指针的值转换为与指针同类型的样式,才可完成赋值,如:int* pn = (int*)100;
2.指针类型与整型加减
公式:指针加减 n 后首地址将向前或后移动 n * 步长 个字节,其中步长为sizeof(目标数据对象)。
例子:
#include <stdio.h>
int main()
{
char* pc = (char*)100;
int* pn = (int*)100;
int* ps = (int*)100;
pc = pc + 1;
pn = pn + 1;
ps = ps - 1;
printf("pc=%u",pc);
printf("pn=%u",pn);
printf("ps=%u",ps);
return 0;
}
结果:pc=101; pn=104; ps=96;
3.同类型指针减法运算
指针类型与指针类型相减后,其结果为两首地址差除以步长。
例子:
#include <stdio.h>
int main()
{
int* pn = (int*)100;
int* pc = (int*)120;
printf("%d",pc-pn);
return 0;
}
结果: 5
另外,由于指针类型与指针类型相加、相乘、相除没有实际意义,C语言中无法通过编译。
四、多级指针
开始之前,先回想之前的提醒,注意类型!因为如果内容要通过编译,等号两边的类型必须一致。而从这一节开始,我们对类型的灵敏度要越来越高,这样学起来才不会乱。
1.指针的指针
看下列代码:
#include <stdio.h>
int main()
{
int n = 123;
int* pn = &n;
int** pnn = &pn;
printf("%d",**pnn);
return 0;
}
结果; 123
我们接下来分析上述代码并灌输知识
a. pnn 是 pn 这一指针的指针,获取了指针 pn 的首地址及其空间大小。
b. pnn 的类型为 int** , 是一个二级指针。
c. 为什么 **pnn 的值为123呢,我们看看其取值过程。首先,对pnn使用取值运算符,将int****还原为int * 类型,接着对 *pnn 使用取值运算符,将 int * 还原为 int 类型。即,还原为 n .
通俗点讲,这其实就是一条有指向的链子,无论有多少个指针,只要指针与指针之间,指针与整形之间相互连接,都可以通过分析其类型来找到一一对应的关系,所以弄清楚类型是特别重要的方法。
2.多级指针
直接上代码:
#include <stdio.h>
int main()
{
int n = 100;
int* one = &n;
int** two = &one;
int*** three = &two;
int**** four = &three;
int***** five = &four;
printf("%d",*****five);
return 0;
}
结果:100
five 的类型为 int* * * * * ,对其使用五次取值运算符将其还原为 int 类型,即 n 的值。
五、指针与数组
1.用指针访问数组
既然指针有访问首地址的功能,那么其是否可以逐一访问数组的每一个元素呢?
请看下列代码:
#include <stdio.h>
int main()
{
int a[3] = {1,2,3,};
int* p = &a[0];
printf("%d",*p);
printf("%d",*(p+1));
printf("%d",*(p+2));
return 0;
}
结果; 1; 2; 3;
分析:a是一个 int 类型的数组,每一个元素占四个字节,p 是 a[0] 这一元素的指针,指向 a[0] ,*p的值即为 a[0] 的值,p+1 则在 p 的基础上向后移动4个字节,正好指向 a[1] 的首地址,所以 p+1 是 a[1] 的指针,p+1 的值即为 a[1] 的值,以此类推。
注意:表达式 p+1 必须先被括号包裹,再使用取值运算符*。这是因为取值运算符 * 的优先级高于算术运算符,我们需要先让首地址移动,再进行取值操作。
2.使用数组名获取数组地址
使用数组名获取数组首地址能显得更加方便。
看下列代码:
#include <stdio.h>
int main()
{
int a[3] = {1,2,3};
int* p = a;
printf("a=%u",a);
printf("&a[0]=%u",&a[0]);
printf("p=%u",p);
return 0;
}
结果:a=5201314; &a[0]=5201314; p=5201314;
分析:首先 a 的值与 &a[0] 的值相等,都表示首地址,其次, a可以将值赋予给指针 p ,因此我们可以得知,a 的类型为 int* ,是一个指针,与 &a[0] 有着相同的作用。
知识灌输:
a. 当数组名 a 出现在一个表达式当中,数组名 a 将会被转换为指向数组第一个元素的指针。不过有两个例外:
对数组名使用 sizeof 时;对数组名使用 & 时。
3.使用指针访问数组等价于下标访问
a. 中括号 [ ] ,被称作下标运算符,它的优先级高于一切其他运算符。
b. 数组名[下标] 等价于 *(数组名 + 偏移量)
例如: a[1] == *(a+1)
六、指针数组
1.指针与指针数组的关系
a. int* pn 是一个指针,类型为 int* , int* pc[3] 是一个指针数组,类型为 int* [3] 。
b. 指针数组是一个存放着指针的数组。
2.指针数组实例展示
看下列代码:
#include <stdio.h>
int main()
{
int n1[3] = {1,2,3};
int n2[3] = {11,22,33};
int n3[3] = {111,222,333}; //对三个数组进行命名
int* pn[3]; //定义了一个含有三个元素的指针数组
pn[0] = n1; //指针数组第一个元素是 pn[0] ,是一个指针,获取了数组 n1 的首地址
pn[1] = n2; //指针数组第二个元素是 pn[1] ,是一个指针,获取了数组 n2 的首地址
pn[2] = n3; //指针数组第三个元素是 pn[2] ,是一个指针,获取了数组 n3 的首地址
for(int i = 0 ; i<3 ; i++)
{
int** p = pn + i; //pn 是一个数组,在表达式中转换为指针,类型是 int** ,p 指向了 pn + i 的首地址
for(int j = 0 ; j<3 ; j++)
{
printf("%d ",*(*p+j)); //*p 类型为 int* ,是一个指针类型,当i=0时,*p+j 指向数组 n1 中 n1[j] 的首地址,*(*p+j) 的值则是 n1[j] 的值
}
printf("\n");
}
return 0;
}
结果: 1 2 3
11 22 33
111 222 333
通过指针数组,我们便可以访问多个数组的每一个元素了。
七、数组指针
1.数组指针的形式
定义一个二维数组 int b[3] [4] , 则 b 的类型为 int[3] [4] , 如果定义指针 pb 指向数 b ,那么定义如下
**int (*pb)[4] = b **
注:括号的用途是防止数组指针变为指针数组。
2.数组指针的移动与取值
看下列代码:
#include <stdio.h>
int main()
{
int b[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11},
{12,13,14,15},
};
int(*pb)[4]=b; //pb 是二维数组b的指针,*pb是一个数组,pb+1 移动了 4*4=16 个字节。
int* p = *pb //此时 p 是数组 *pb 的指针,p+1 移动4个字节。
printf("%d",*(pb));
printf("%d",*(pb+1));
printf("%d",*(pb+2)); //一行一行地访问
printf("%d",*p);
printf("%d",*(p+1));
printf("%d",*(p+2));
printf("%d",*(p+3)); //一个一个的元素进行访问
}
结果:0; 4; 8; 0; 1; 2; 3;
3.指针访问与下标访问等价
*指针[下标] = (指针 + 偏移量)
即 上述 *(pb+1) 可写成 pb[1]
八、声名顺序
对于数组、函数、指针,它们遵循下列的运算数序,优先级从高到低:
1、括号()
2、函数声明的( ) 与数组声明的()优先级相同
3、指针 声明的*
九、指针作为参数传递
1.指针能跨函数传递
由于被调函数的形参无法改变主调函数的变量,我们可以通过指针来进行数据传输。
看下列代码:
#include <stdio.h>
void swap(int* x , int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int a,b;
a = 1;
b = 2;
swap(&a,&b);
printf("a=%d , b=%d",a,b);
return 0;
}
结果: a=2 , b=1;
因此,可以通过指针来传递参数并改变主调函数的变量。
2.仅有首地址的指针类型void*
性质:仅保存首地址,不保存目标的空间大小,不能进行取值与加减运算。但任意类型的指针都可以直接赋值给它。
十、函数与指针
1.从函数中返回指针
看下列代码:
#include <stdio.h>
int* fun() //返回的是指针,函数类型也是指针
{
static int n = 100; //static 保证 n 在第一次被使用完后不被回收
n++
return &n;
}
int main()
{
int* p = fun();
printf("%d",*p);
fun(); //再次调用被调函数
printf("%d",*p);
return 0;
}
结果:100; 101;
2.初始化指针并从函数中返回多个变量
看下列代码:
#include <stdio.h>
void func(int**a,int**b)
{
static int x = 100;
static int y = 200;
*a = &x;
*b = &y;
}
int main()
{
int* a = NULL; //NULL 可以初始化指针,将指针内保存的地址设置为0
int* a = NULL;
func(&a,&b);
printf("a=%d,b=%d",*a,*b); //将指针的指针传入被调函数
return 0;
}
结果:*a=100; *b=200;
十一、函数指针
1.使用指向函数的指针
看下列代码:
#include <stdio.h>
int print(char*pc)
{
int count=0;
while(*pc!='\0') //刚开始 *pc 为 h
{
putchar(*pc);
pc++;
count++;
}
putchar('\n');
return count;
}
int main()
{
int(*p)(char*)=print; //函数的指针的写法
int n = (*p)("helloworld");
printf("%d",n);
return 0;
}
结果:helloworld
10;
十二、字符串与字符指针
1.字符数组与指针
看下列代码:
#include <stdio.h>
int main()
{
char str[] = "alniubi";
char* p = str;
while(*p != '\0')
{
*p-=32;
p++;
}
puts(str);
return 0;
}
结果:ALNIUBI;
用指针指向字符串和数组类似
十三、const关键词
1.const的作用
const 修饰了某个类型,则该类型无法再被改变
2.const修饰指针本身
a. char const *str = “hello”;
b. const char *str = “hello”;
c. char * const str = “hello”;
对于以上三种表述,a 和 b 同类,const 在 * 左边修饰指针指向的数据 ,此时 str 可以修改,“hello” 不能修改。
c 则不同,const 修饰右边指针本身。
intf(“%d”,n);
return 0;
}
结果:helloworld
10;
## 十二、字符串与字符指针
### 1.字符数组与指针
看下列代码:
```c++
#include <stdio.h>
int main()
{
char str[] = "alniubi";
char* p = str;
while(*p != '\0')
{
*p-=32;
p++;
}
puts(str);
return 0;
}
结果:ALNIUBI;
用指针指向字符串和数组类似
十三、const关键词
1.const的作用
const 修饰了某个类型,则该类型无法再被改变
2.const修饰指针本身
a. char const *str = “hello”;
b. const char *str = “hello”;
c. char * const str = “hello”;
对于以上三种表述,a 和 b 同类,const 在 * 左边修饰指针指向的数据 ,此时 str 可以修改,“hello” 不能修改。
c 则不同,const 修饰右边指针本身。