目录
一、指针的概念
1、内存和地址
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。其实也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节。
一个字节空间能放8个比特位,每个内存单元都有一个编号(这个编号相当于宿舍的门牌号),有了这个内存单元的编号,CPU就能快速找到这个内存空间。
生活中我们也把门牌号叫做地址,计算机中我们把内存单元的编号也叫地址。C语言中把地址取了个新名字叫:指针。
则有:编号 == 地址 == 指针
2、&取地址操作符
C语言中创建变量就是向内存申请空间。
比如,上述的代码就是创建了整型变量a,内存中申请4个字节,用于存放整数10,其中每个字节都有地址。前面的编号就是地址。
而这些地址可以通过&操作符得到。&a取出的是a所占4个字节中地址较小的字节的地址。
3、指针变量和解引用操作符(*)
通过取地址操作符(&)拿到的地址是⼀个数值,比如上面的0019FCCC,这个数值有时也要存储起来,存放在指针变量中。
比如:
#include <stdio.h>
int main()
{
int a = 10;
int * pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
这里pa左边写的是 int* , * 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int) 类型的对象。
解引用:
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;//解引用操作
return 0;
}
*pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa=0,这个操作符是把a改成了0。
4、指针变量大小
- 指针变量的大小取决于地址的大小
- 32位平台下地址是32个bit位(即4个字节)
- 64位平台下地址是64个bit位(即8个字节)
#include <stdio.h>
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
5、指针变量类型的意义
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如: char* 的指针解引用就只能访问⼀个字节,而int* 的指针的解引用就能访问四个字节
6、指针初始化和野指针
初始化:
int* pa=NULL;
野指针:
1、指针未初始化
2、指针越界访问
3、指针指向的空间被释放
7、指针运算
7.1、指针+、-整数
比如:
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];//拿到第一个元素地址
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
因为数组在内存中是连续存放的,只要知道第一个元素地址,就能找到后面的所有元素。
7.2、指针 - 指针
结论:指针-指针得到的是两个指针之间元素的个数,不过前提是要两个指针要指向同一块空间。
//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
7.3、指针的关系运算
指针是可以比较大小的。
比如:
//指针的关系运算
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int sz = sizeof(arr)/sizeof(arr[0]);
while(p < arr + sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针相比较,但不允许与指向第一个元素之前的那个内存位置的指针进行比较。
8、二级指针
-
二级指针是一个指针变量,它存储的是另一个指针的地址。
-
例如,
int** p
表示p
是一个二级指针,它指向一个int*
类型的指针。
比如:
int x = 10;
int* p = &x;
int** pp = &p;
printf("%d\n", **pp); // 输出 10
二、深入理解指针
1、const修饰指针
const是一个关键字,用于声明常量,表示某个变量或对象的值在初始化后不能被修改.
1.1、const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但在变量前加上const,这个变量就不能被修改。
比如:
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的 ,直接修改编译器会报错
return 0;
}
但如果我们取出n的地址,使用n的地址去修改n是可行的。
比如:
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int* p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
运行结果:
所以这样就突破了const的限制,有一定危险。
1.2、const修饰指针变量
int * p;//没有const修饰?
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰
结论:const修饰指针变量的时候
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变。
• const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
比如:
int a = 10;
const int *p = &a; // p指向一个整型常量
*p = 20; // 错误:不能通过p修改a的值
p = &a; // 正确:可以修改p的指向
int a = 10;
int *const p = &a; // p本身是一个常量指针
p = &a; // 错误:不能修改p的指向
*p = 20; // 正确:可以通过p修改a的值
2、assert(断言)
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
语法:条
#include <assert.h>
assert(条件);
如果条件为真,程序继续执行;如果条件为假,程序会终止运行,并输出错误信息,包括文件名、行号以及失败的条件。
比如:
#include <assert.h>
#include <stdio.h>
int main()
{
int a = 10;
assert(a > 0); // 如果a <= 0,程序会终止并报错
return 0;
}
断言主要用于开发和调试阶段(debug版本),帮助开发者快速发现和定位问题。
3、指针的传值调用和传址调用
3.1传值调用
void increment(int x)
{
x++; // 修改的是x的副本,不影响实际参数
}
int main()
{
int a = 10;
increment(a);
printf("%d\n", a); // 输出10,因为a的值没有被修改
return 0;
}
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
3.2、传址调用
void changePointer(int **p)
{
*p = NULL; // 修改指针本身的值
}
int main()
{
int a = 10;
int *ptr = &a;
changePointer(&ptr); // 传递ptr的地址
printf("%p\n", ptr); // 输出NULL,因为ptr被修改
return 0;
}
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。
三、指针与数组
1、数组名的理解
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
输出结果:
则有:数组名就是数组首元素(第⼀个元素)的地址。
但也有例外:
- sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小, 单位是字节。
- &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
2. 使用指针访问数组
#include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
}
//输出
for(i=0; i<sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)。
3. ⼀维数组传参的本质
#include <stdio.h>
void test(int arr[])//也可以写成int* arr
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
打印结果:
数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
4、指针数组
本质:是数组,存放指针的数组。
比如:
int* arr[]={......};
假设用一维数组模拟出一个3行4列的二维数组:
int a[]={1,2,3,4};
int b[]={2,3,4,5};
int c[]={3,4,5,6};
int* arr[3]={a,b,c};//每个元素用来存放三个数组首元素的地址
for(int i=0;i<3;i++)
{
int j=0;
for(j=0;j<4;j++)
{
printf("%d ",arr[i][j]);//等于printf("%d ",*(arr[i]+j))
}
printf("\n");
}
4.1、字符指针
是用来存放字符的指针。
char c = 'A';
char* pc = &c;//pc是一个字符指针,它存储了变量c的地址。
通过字符指针可以访问字符串中的单个字符,比如:
char str[] = "Hello";
char* ptr = str;
printf("%c\n", *ptr); // 输出 H
printf("%c\n", *(ptr + 1)); // 输出 e
printf("%c\n", *(ptr + 4)); // 输出 o
4.2、字符指针与数组的区别
虽然字符指针和字符数组都可以用于处理字符串,但它们在内存布局和使用方式上有很大区别:
(1)字符数组
-
定义:
char str[10] = "Hello";
-
内存分配:分配固定大小的内存空间(10个字符)。
-
特点:数组名
str
是一个常量指针,指向数组的第一个元素,不能修改。 -
使用:可以直接存储字符串,但大小固定。
(2)字符指针
-
定义:
char* ptr = "Hello";
-
内存分配:指针本身只存储地址,指向的字符串存储在其他地方(可能是常量区)。
-
特点:指针可以修改,指向不同的字符串。
-
使用:可以动态分配内存(如
malloc
),但需要手动管理内存。
5、数组指针
本质:是指针,存放数组地址的指针----指向数组的地址。
比如:int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,每个数组元素类型为int。[ ]的优先级高于*,别忘加括号
6、&数组名 VS 数组名
- 数组名--数组首元素地址
- &数组名--取的是整个数组的地址
- 数组首元素的地址和数组的地址从值的角度来看是一样的,但意义不一样。
比如:
#include <stdio.h>
int main()
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*p)[10] = &arr;
printf("%p\n", arr); // 输出数组arr的首地址(等价于&arr[0])
printf("%p\n", arr + 1); // 输出数组arr的第二个元素的地址(arr[1]的地址)
printf("%p\n", &arr[0]); // 输出数组arr的第一个元素的地址(等价于arr)
printf("%p\n", &arr[0] + 1); // 输出数组arr的第一个元素地址加1后的地址(arr[1]的地址)
printf("%p\n", &arr); // 输出整个数组arr的地址(与arr的首地址相同)
printf("%p\n", &arr + 1); // 输出数组arr地址加1后的地址(跳过整个数组的大小)
return 0;
}
对二维数组来说,数组名表示第一行的地址。比如:
四、函数指针
本质:是指针,存放函数地址的指针变量--指向函数。
语法:返回类型 (*指针名称)(参数类型列表);
比如:
int Add(int x,int y)
{
return x+y;
}
int main()
{
int (*pf)(int,int)=&Add;
return 0;
}
&函数名和函数名都是函数的地址。
又比如:
int main()
{
void (*signal(int,void(*)(int)))(int);
return 0;
}
分开点来看:
void (* signal (int,void (*)(int) ) ) (int);
这段代码是一次函数声明,声明的函数名叫signal,signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,该函数指针能够指向的那个函数的参数是int,返回类型是void。signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void。
五、函数指针数组
本质:是数组,存放函数地址的数组
语法格式:返回类型 (*数组名[数组大小])(参数类型列表);
比如:
int (*parr[10])();
parr先和[ ]结合,说明parr是数组,数组的内容是int (*)()类型的函数指针。
六、指向函数指针数组的指针
比如:
int (*(*ppf)pf[10])(int,int);
ppf是指向函数指针数组的指针。
-
最内层的括号:
(*ppf)
表示ppf
是一个指针。 -
中间的括号:
(*ppf)pf
表示pf
是一个数组,数组的每个元素都是一个函数指针。 -
最外层的括号:
int (*(*ppf)pf[10])(int,int)
表示这个数组包含10个函数指针,每个函数指针指向一个返回int
且参数为两个int
的函数。