0.引入
对象的访问方式
1. 直接访问 : 通过对象名来进行访问
缺点:受作用域的限制
2. 间接访问: 通过对象的地址来进行访问
指针
0.0内存地址
- 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
0.1基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,期地址是其所有字节中编号最小的那个,称为基地址。
0.2取址符
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址
- 单目运算符 操作数:可写的空间
- 例如:
int a;
printf("%p\n", &a);
- 注意:
- 虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸确实一样的。
- 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
注意:
只要都是指针, 所占空间大小一样
都只是把保存一块空间的基地址编号
1.指针基础
-
指针的概念:
-
一个专门用来存放内存地址的变量,指针也就是指针变量
存储单元(如: memory, 内存)的地址:
分配给每个变量的内存单元都有一个编号,这个编号就是我们说的存储单元的地址。并且存储单元按字节来编址。
-
-
地址。比如 &a 是一个地址,也可以看做是一个指针,&a: 指向变量 a
- 专门用于存储地址的变量,又称指针变量。
-
格式
-
指向的对象的类型 *指针变量名
-
解释:
* :表示那个变量是一个指针变量
- “指向的类型” : 指针变量指向的内存空间存放的数据类型
- “指向” : 如果我保存了你的地址,那么就说我指向你
- “*” :定义指针变量的固定格式
-
-
指针的定义:
指向的对象的类型 *变量名;
eg:
//有一个整型变量a, 定义一个指针p来保存 a的地址
int * p;// 整型指针
char* p1;//字符型指针
double *p2;
- 指针的赋值:赋给指针的地址,类型需跟指针的类型相匹配。
int a = 100;
int *p = &a;
char b;
char *p1 = &b;
-
指针的索引:通过指针,取得其指向的目标
* :间接运算符, 单目运算符 (*地址) 《==》 该地址对应的那个对象 ☆☆☆ 既能做左值,又能做右值 int a = 10; int *p = &a; (*p) <==> p指向的那个对象 =========== int a; *&a <==> a *& 可以直接省略
代码分析:
#include <stdio.h>
{
int a = 10;
//定义一个指针变量p, 保存a的地址
int *p = &a;// int *p; p = &a;
printf("%p\n", &a);
printf("%p %p\n", p, &p);
*p = 100;// *p <==> *&a <==>a a = 100
printf("a = %d\n", a);
*&a = 1000;// a = 1000;
printf("a = %d\n", a);
int b = *p;
}
-
指针的尺寸
- 指针尺寸指的是指针所占内存的字节数
- 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
- 结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关
- 在32位系统,指针的大小占用4字节
- 在64位系统,指针的大小占用8字节
// 指针大小 char *p; int *p1; double *p2; sizeof(p) == sizeof(p1) == sizeof(p2) 在32bits上都是4个字节 =========================== #include <stdio.h> int main() { char a; int b; double c; char *p = &a; int *p1 = &b; double *p2 = &c; printf("%d %d %d\n", sizeof(p), sizeof(p1),sizeof(p2));// 8 8 8 int *p3, *d, e;// int *p3; int *d; int e; printf("%d %d %d\n", sizeof(p3), sizeof(d),sizeof(e));// 8 4 4 }
int main()
{
int a[10] = {260,2,3,4,5,6};
char *p_a = &a;
*p_a = 10;// 跟着指针p_a 能够操作的空间大小为:1字节
for(int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
2.把地址/ 指针变量作为函数传参
传的还是“值”,传的还是“实参的值”,
“实参的值可能是某个对象的地址”
C语言中函数参数的传递只能是“传值调用”
形参 = 实参的值 !!!
“值” 可以是数值 也可以是 地址
练习:
写一个函数交换两个变量的值
#include <stdio.h>
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
void swap_1(int*x, int *y)
{
int *t = x;
x = y;
y = t;
}
void swap_2(int*x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
void swap_3(int*x, int *y)
{
//int a;
int *t ;// 野指针
*t = *x;
*x = *y;
*y = *t;
}
int main()
{
int a = 3, b = 5;
//swap(a, b);
//swap_1(&a, &b);
//swap_2(&a, &b);
swap_3(&a, &b);
printf("%d %d\n", a, b);
}
3.野指针
-
概念:指向一块未知区域的指针,被称为野指针。野指针是危险的。
-
危害:
- 引用野指针,相当于访问了非法的内存,可能会导致段错误(segmentation fault)
- 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
-
产生原因:
-
指针定义之后,未初始化
int *p;// 定义一个野指针
-
指针所指向的内存,被系统回收
int* Func() { int a; return &a; } int main() { int*p = Func();//p指向了一块释放的空间 } //注意: 不允许返回一个作用域是随函数的持续性的局部变量的地址 int* Func() { static int a; return &a; } int main() { int*p = Func();//p指向了一块释放的空间 } //======================== int* Func(int n) { static int a = 0;//注意: static 变量初始化只能是一个常量 a = n; return &a; } int main() { int*p = Func(1); printf("*p = %d\n", *p); int *q = Func(2); printf("*q = %d\n", *q); }
-
指针越界
char a; int *p = &a; *p ==> 此时所对应的空间有3个字节是越界的
-
-
如何防止:
- 指针定义时,及时初始化
- 绝不引用已被系统回收的内存
- 确认所申请的内存边界,谨防越界
-
☆☆☆☆注意: 操作野指针很危险, 操作 : 操作野指针指向的空间
4.空指针
很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。
- 概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
- 示例:
#define NULL (void*)0 ===> 在stdio.h 文件中已声明
int *p = NULL;
=================================
#include <stdio.h>
int main()
{
int *p = NULL;
if(p)
{
*p = 100;
printf("*p = %d\n", *p);
}
int a;
p = &a;
if(p)
{
*p = 100;
printf("*p = %d\n", *p);
}
}
注意: 操作空指针, 必出段错误
5.数组与指针
数组元素与普通变量是一样的,数组元素也有自己的地址。
数组元素也有左值和右值,并且数组元素间的地址是相邻的。
数组名可以代表首元素的地址.
a => &a[0], when 数组名a当作指针来用!!!
数组名 是 保存第一个元素的地址的指针
如:
int a[4] = {0};
int*p = &a[2];
*p = 100;
===> a: 0 0 100 0
//=====================
int a[4] = {1,2,3,4};
int*p = a;// int *p = &a[0]
*p = 100;
printf("a[0] = %d\n", a[0]);
6. 指针的类型
1. 从定义格式看
指向的对象的类型 *
int *p;
typeof(p) ==> int*
2. &x ==> 看做是一个指针
x 是任何对象
typeof(&x) ==> typeof(x)*
7.指针运算
指针作加减的问题:
p + i (p是一个指针,i是一个整数) 不是简单地加减数值,
而是加减i个指向单元的长度.
指向的单元的长度:指向的对象类型所占的空间大小
注意:指针作加减还是指针
- 指针加法意味着地址向上移动若干个目标
- 指针减法意味着地址向下移动若干个目标
- 示例:
int a[4];
char*q = &a[1];// q指向的对象 占1个字节 q+1 ==>往后挪 1个char
printf("q = %p, q+1 = %p\n", q, q+1);
int *p = &a[1]; // p 指向的对象 占4个字节 p+1 ==>往后挪 1个int
printf("p = %p, p+1 = %p\n", p, p+1);
注意:
指针与指针作减法运算: 相差的单元长度的个数
两个指针作加法 无意义
两个指针可以 关系运算符操作
- 指针运算
int a[4];
int *p = &a[0];
p+1 ==> 加了1个int
typeof(p) ==> int *
p+1 <==> &a[1]
*(p+1) <==> a[1]
int *q = &a[2];
*(q+1) <==> a[3]
-
指针偏移
- 指针的加减称为指针的偏移,偏移的单位由指针的类型大小所决定所谓指针的类型大小指的是指针变量所指向的内存空间的数据类型
-
数组与指针
-
数组名有两个含义
-
第一个含义,表示整个数组
int a[4]; sizeof(a) ==> 16 typeof(a) ==>int[4]
-
第二个含义,首元素的地址
printf("%d\n", a);//输出的就是首元素的地址 scanf("%d", a);// 看作指针 &a[0] int *p = a; a+1 ==> &a[0] + 1 ==> &a[1] typeof(&a[0]) ==> int* *(a+1) ==> a[1]
-
注意: 在代码中遇到数组名: 如果既能看作数组又能看作指针,编译器默认看作数组(看定义)
总结:数组最后编译器会自动转为指针操作,数组运算其实就是指针运算
================================
int a[4]; a[0] <==> *(a+0) a[1] <==> *(a + 1)
-
练习1:
int array[5] = {10,20,30,40,50};
//要求定义一个指针p存储数组的首地址,通过数组名或者指针将所有获取到40数据的方法罗列出来
#include <stdio.h>
int main()
{
int a[5] = {10,20,30,40,50};
//要求定义一个指针p存储数组的首地址
//通过数组名或者指针将所有获取到40数据的方法罗列出来
int *p = a;// p = &a[0]
printf("a[3] = %d\n", a[3]);
printf("*(a+3) = %d\n", *(a+3));
printf("p[3] = %d\n", p[3]);
printf("*&p[3] = %d\n", *&p[3]);
printf("*(p+3) = %d\n", *(p+3));
printf("*&a[3] = %d\n", *&a[3]);
}
总结 :
1.指针一定要指向一块合法的空间,否则可能出现段错误,没有空间自行分配
2.可以将指针转换成数组使用:指针指向一块空间,可以将这块空间当作数组空间使用,不会去管该空间能不能用
int a[4] = {0};
int *p = a;//a 看作是指针
p[1] += 2;// *(p+1) <=> a[1]
p += 4;
p[1] += 2;
printf("a[1] = %d\n", a[1]);
printf("p[1] = %d\n", p[1]);
-
只要数组名在作加减,此时数组名一定是看做是指针
保存第一个元素地址的指针☆☆☆数组名永远都只能保存第一个元素的地址,不可更改
int a[4] = {1};
a++;
int *p = a;
*p = 1024;
for(int i = 0; i < 4; i++)
{
printf("%d ", a[i]);
}
假设指针p 指向的是一个一维数组的首地址
*(a+i) <==> a[i] <==> *(p+i) <==> p[i]
8.指针常量和常量指针
const :关键字 只读
int const a;//const int a;
const int a;// a所开辟的空间本身具有可读可写,是跟着a去操作才带有只读属性
//a = 10;//编译错误
int *p = &a;
*p = 10;// 可修改: const 修饰的是 跟着a去操作才带有只读属性
printf("a = %d\n", a);
1.指针常量 :本质上是一个常量, 指针类型的常量
本身不可更改,但是指向的空间可以更改
int a = 1, b = 1;
int* const p = &a;// p此时是一个指针常量,本身不可改,指向的对象可改 要记得初始化
//p = &a;
*p = 10;
p = &b;
=====================
2.常量指针
是一个指针变量, 指向的对象不可更改
int a = 1;
int const *p;//常量指针:指向的对象不可改, 本身可改
p = &a;
//*p = 100;
a = 100;
printf("a = %d\n", a);
const int *p;// //常量指针:指向的对象不可改, 本身可改
const int*const p;// 一个指针常量指向一个不可更改的对象
===========================================
9.再论数组名与指针
数组名有两个含义:
1. 代表整个整组
2. 代表首元素的地址
数组名 看作是指针:首元素的地址
数组名[0]
数组基地址编号:X
1. int a[10] = {1,2,3,4,5,6,7,8,9};
printf("a = %p, a+1 = %p, &a + 1 = %p\n", a, a+1, &a+1);
a : 看作是指针 &a[0] 地址编号 :X
a+1 : a看作是指针 &a[0] 地址编号 :X+4
&a[0] + 1 ==> &a[1] 在原先的基础上 往后挪了1个int
==> typeof(&a[0]) ==> typeof(a[0])* ==>int*
&a+1: a 代表整个整组 地址编号 :X+40
&a + 1 ==> 在原先的基础上 往后挪了1个int[10]
typeof(&a) ==> typeof(a)* ==>int[10]*
2. int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
printf("a = %p, a+1 = %p, &a[0] + 1 = %p, &a + 1 = %p\n", a, a+1, &a[0]+1, &a + 1);
a : 看作是指针 &a[0] 地址编号 :X
a+1 : a看作是指针 &a[0]+1 ==> &a[1] 地址编号 :X+16
typeof(&a[0]) ==> typeof(a[0])* ==>int[4]*
typeof(&a[0]+1) ==>int[4]*
&a[0]+1 ==> &a[1] 地址编号 :X+16
&a + 1 : a 代表整个数组 &a+1 加了 1个int[4][3] 地址编号 :X+48
typeof(&a) ==> typeof(a)*
==> int[4][3]*
10.多维数组与指针
(1)数组名可以看作是指向第一个元素的指针,并且在数值上为第一个元素的地址。
数组名a, if 把a当指针
a => &a[0]
(2) C语言所有数组都是一维数组!
int a[3][4];
a 里面有 3个元素, 每个元素都是一个int[4] 类型的对象
a :里面有3个数组,分别对应 a[0] a[1] a[2]
a[0],a[1],a[2] 都是该数组的数组名
eg:
int a[3][4];
表达式 表达式的含义及类型 表达式的值
a 数组名 首元素的编号 &a[0]
1. 看作是指针:首元素的地址 &a[0] 在数值上:&a[0]=>&a[0][0]=>a=>&a
typeof(a) ==>typeof(&a[0]) 假设编号:X
typeof(a[0])*=>int[4]*
2. 代表整个数组
typeof(a) => int[4][3]
&a[0] 取第一行的地址 第一行的地址 &a[0]
取a[0]这个元素的地址 在数值上:&a[0]=>&a[0][0]=>a=>&a
typeof(&a[0])==>typeof(a[0])* 假设编号:X
==> int[4]* (默认代表数组)
a[0]:代表整个数组 typeof(a[0])==>int[4]
a[0]:看作是指针 &a[0][0],typeof(&a[0][0])==>int*
a[0] 相当于一个一维数组的数组名 &a[0][0]
在数值上:&a[0]=>&a[0][0]=>a=>&a
假设编号:X
1. a[0]:代表整个数组 typeof(a[0])==>int[4]
2. a[0]:看作是指针 &a[0][0],typeof(&a[0][0])==>int*
&a[0][0] a[0][0] 这个元素的地址 &a[0][0]
typeof(&a[0][0]) ==> int* 在数值上:&a[0]=>&a[0][0]=>a=>&a
假设编号:X
a+1 取第二行的地址 第一行的地址 &a[1]
取a[1]这个元素的地址 在数值上:&a[1]=>&a[1][0]=>a+1
a : 看作是指针 假设编号:X+16
a+1 ==> &a[0] + 1
==> &a[1]
&a[1]
&a+1 a数组整个挪动 int[4][3]个字节之后, 一个int[4][3]数组的首地址 假设编号:X+48
typeof(&a+1) ==> int[4][3]*
a[1]+2 a[1][2] 这个元素的地址 &a[1][2]
a[1] 看作是指针 &a[1][0] 数值上: X + 24
typeof(&a[1][0]) ==> int*
a[1] + 2 ==> &a[1][2]
typeof(a[1]+2) ==> typeof(&a[1][2])
==> int*
*(a+1)+2 a[1][2] 这个元素的地址 &a[1][2]
typeof(*(a+1)+2) ==>typeof(&a[1][2])==>int* 数值上: X + 24
*(a+1)+2
==> *(&a[0] + 1) + 2
==> *(&a[1])+2
==> a[1]+2
==> &a[1][0] + 2 ==> &a[1][2]
*(a[1]+2) 代表 a[1][2]这个元素 输出:a[1][2]元素的值
*(a[1]+2)
==> *(&a[1][0] + 2)
==> *(&a[1][2])
==> a[1][2]
*(*(a+1)+2) 代表 a[1][2]这个元素 输出:a[1][2]元素的值
*(*(a+1)+2)
==> *(*(&a[0]+1)+2)
==> *(*(&a[1])+2)
==> *(a[1]+2)
==> *(&a[1][0]+2)
==> *&a[1][2]
==> a[1][2]
结论:
a[N][M] (0 <= i < N, 0 <= j < M)
a+i ==> &a[i]
*(a+i) ==> a[i]
*(a+i)+j ==> a[i]+j ==> &a[i][j]
*(*(a+i)+j) ==> a[i][j]
a[i] + j ==> &a[i][j]
*((a[i])+j) ==> a[i][j]
11.数组指针和指针数组
- 数组指针
- 本质上是一个指针,指向的对象是一个数组
int a[4];
// 定义一个指针变量p,保存a的地址
typeof(a) *p;
==> int[4] *p = &a;
==> int(*p)[4] = &a;// 此时 p就是一个数组指针
typeof(p) ==> int[4]*
p+1 ==> 加了一个 int[4]
*(*p+1) = 100;
*(a+1) ==> *(&a[0] + 1) ==>*&a[1] ==> a[1]
// *p <==> 代表p所指向的对象 *p <===> a
//(*p)[1] = 100;// ==> a[1] = 100
*p <===> *(p+0) <==> p[0] <==> a
// (*p)[1] = 100;// ==> p[0][1] = 100 ==> a[1] = 100
================================
#include <stdio.h>
int main()
{
int a[4] = {1,2,3,4};
// 定义一个指针变量p,保存a的地址
//typeof(a) *p = &a;
int (*p)[4] = &a;// typeof(p) ==> int[4]*
//(*p)[1] = 100;// 因为p保存的是整个数组a的地址,此时 *p 就是数组名
//(*p)[1] = 100;// a[1] = 100
//p[0][1] = 100;// p指向一块空间, 看作数组名, 里面每个元素都是int[4]
//printf("%d\n", sizeof((*p)+1));//
//*((*p)+1) = 100;
*(p[0]+1) = 100;
//*(*p+1) = 100;
for(int i = 0; i < 4; i++)
printf("%d ", a[i]);
printf("\n");
}
#include <stdio.h>
int main()
{
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int(*p)[4] = a;// p = &a[0];
//*p <==> *(p+0) <==> a[0] <==> p[0]
printf("%d\n", p[0][0]);
printf("%d\n", (*(p+2))[2]);// (*(p+2))[2] ==> (*(&a[2]))[2] ==> a[2][3]
printf("%d\n", *(p[1]+2));// *(p[1]+2) ==> *(a[1]+2) ==> *(&a[1][0] + 2) ==> a[1][2]
printf("%d\n", *(*(p+2)+2));
}
===========================================
#include <stdio.h>
int main()
{
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
//定义一个指针变量p,保存a的地址
int (*p)[3][4] = &a;// int[4][3]*
printf("sizeof(*p) = %ld\n", sizeof(*p));
printf("sizeof((*p)[0]) = %ld\n", sizeof((*p)[0]));
}
- 指针数组
- 本质上是一个数组:里面的元素类型都是指针类型
int a[4] = {1,2,3,4};
int *p[4];// p 就是一个数组, 数组中有4个int*的元素
typeof(p) ==> int*[4]
sizeof(p) ==> 32
p[0] = a;// p[0] = &a[0]
*p[0] <==> a[0]
*p <==> *&p[0] <==> p[0]
**p <==> a[0]
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=assets%2F1713249548887.png&pos_id=img-hcTgSGGb-1721048886121)
12.指针强转
指针做强转,注意指针所指向的空间大小,不管空间怎么变化,都是以基地址为首来操作
分析如下代码的结果:
int a[5] = {1,2,3,4,5};
int *ptr = (int*) (&a + 1);
printf("%d %d\n", *(a+1), *(ptr - 1)); //2 5
=========================================
int a[5] = {1,2,3,4,5};
int *ptr = (int*) &a + 1;
printf("%d %d\n", *(a+1), *(ptr - 1));//2 1
int a[] = {1,2};
int *p = a;
printf("%p %d %p %d\n", p, *p, p+1, *(p+1));//x 1 x+4 2
char *p1 = (char*)a;
printf("%p %d %p %d\n", p1, *p1, p1+1, *(p1+1));// x 1 x+1 0
char b[] = {'a', 'b','c','d','e'};
int *q = (int*)b;
printf("%p %d %p %d\n", q, *q, q+1, *(q+1));//x 97 x+4 未知
练习2:
有这么一个十六进制数据int data = 0x11223344,定义指针指向data然后通过指针把22打印出来
int data = 0x11ff3344;
char*p = (char*)&data;// char* = int*
unsigned char b = *(p+2);
printf("%x\n", (unsigned char)*(p+2));// ff ==> int
printf("%x\n", (unsigned char)p[2]);
=================================================
printf("%x\n", (data >> 16)&0xff);
有这么一个数组:char a[] = {0x11,0x22,0x33,0x44},把这个数组中的数据合并成一个int型数据
其值为 0x11223344
char a[] = {0x11,0x22,0x33,0x44};
char t = a[3];
a[3] = a[0];
a[0] = t;
t = a[1];
a[1] = a[2];
a[2] = yt;
int*p = (int*)a;
int data = *p;
printf("data = %x\n", data);
==========================================
char a[] = {0x11,0x22,0x33,0x44};
int data;
char*p = (char*)&data;
*p = a[3];//p[0] = a[3]
*(p+1) = a[2];
*(p+2) = a[1];
*(p+3) = a[0];
printf("data = %x\n", data);
===========================================
char a[] = {0x11,0x22,0x33,0x44};
int data = (a[0]&0xff) << 24 | (a[1]&0xff) << 16 | (a[2]&0xff) << 8 | (a[3]&0xff);
printf("data = %x\n", data);
-
字节序
- 大端模式:高位数据存放在内存的低地址端,低位数据存放在内存的高地址端
- 小端模式:高位数据存放在内存的高地址端,低位数据存放在内存的低地址端
练习3:判断自身电脑系统到底是大端模式存放数据,还是小端模式存放数据
int data = 0xff010102; char*p = (char*)&data; if(*p == 0xff) { printf("大端\n"); } else { printf("小端\n"); }
13.字符串与指针
C语言,是没有字符串的类型。C语言的字符串是通过char*(字符型指针)来实现的。
C语言的字符串,是用“”(双引号)引起来的一串字符,并且字符串后面默认加一个’\0’(字符串结束标志,ASCII为0).
我们只需要保存字符串的首地址就可以(第一个字符的地址),从首地址开始找到第一个\0,前面的字符就是字符串里面的字符
char *s = "123456";// 把“123456”所在常量空间首地址 赋值 给 s
*(s+1) = 'a';// 运行error 修改只读空间区域 段错误
char s1[] = "123456";
*(s1+1) = 'a';//正确 修改的s1数组自己的空间
s1++;// 编译出错 s1是一个数组名,不可改 s1 = s1+1
*(s1+2) = 'b';
字符串的值 某种意义上就是第一个字符的地址
- 字符串常量在内存中实际就是一个匿名数组
void Func(char*q)
{
//q[5] <==> *(q+5)
}
int main()
{
char*s = "123456";
Func(s);
}