什么是指针?
首先,C语言的目标是对内存的访问。在计算机系统中,CPU和内存是相互依存、密切配合的两个重要组成部分,它们共同决定了计算机的运行效率和性能。CPU主要对指令进行解析,执行,受限于芯片体积,功耗等因素,CPU内部很难做到大存储,这样CPU要执行的指令和数据必须外置,那么CPU如何访问到指令和数据呢?CPU要对它们进行寻址,才能找到它们。C语言给地址起了新名字,称为指针。
#include <stdio.h>
int main() {
int a = 0;
&a; // &被称为取地址操作符,取出a占4个字节中较小字节的地址
int *pa = &a; // 取出a的地址存储到指针变量pa中 pa是int*类型
//指针变量在32位机器中为4个字节(32/8bit),在64位机器中为8个字节
char *pb = (char*)&a; // (char *)让程序知道这是一个指向字符型数据的指针
*pa = 10; // 对于地址的使用,需要解引用操作符*,这里将 a 修改为10
//解引用后访问时与基本数据类型有关,例如:char*访问1个字节,int*访问两个字节
return 0;
}
那么如何描述一个地址?
两个方面,第一:这个空间有多大。第二:这个地址指向的空间如何访问,是一个字节一个字节的访问(即p+1)还是其他形式访问。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
void指针
可以在不知道具体数据类型的情况下,接受和处理任意类型的指针,并根据具体的类型进行相应的操作
void Print(void *ptr, char type) {
if (type == 'i') {
int *intP = (int *)ptr;
printf("%d\n", *intP);
} else if (type == 'f') {
float *floatP = (float *)ptr;
printf("%f\n", *floatP);
} else {
printf("未知类型\n");
}
}
int main() {
int a = 10;
float b = 3.14;
Print(&a, 'i'); // 传递整数类型的指针
Print(&b, 'f'); // 传递浮点数类型的指针
return 0;
}
const修饰指针
1.const修饰变量
应用const对变量加上一个限制,使起不能被修改,但是只是一个建议,实际上可以通过地址进行修改
int main() {
const int n = 0;
printf("n = %d\n", n);// 0
int*p = &n;
*p = 20;
printf("n = %d\n", n);// 20
return 0;
}
2.const修饰指针变量
1. const 放在*的左边:限制的是*p,意思是不能通过p来改变p指向的对象的内容,但是p本身是可以改变的,p可以指向其他对象。 const int *p = &n;
2. const 放在*的右边:限制的是p,意思是不能修改p本身的值,但是p指向的内容是可以通过p来改变的. int * const p = &n;
int main()
{
const int n = 10;
int m = 100;
const int * p = &n;
*p = 20; //err
p = &m; //ok
return 0;
}
int main()
{
const int n = 10;
int m = 100;
const int * const p = &n;
*p = 20; //ok
p = &m; //err
printf("%d\n", n);
return 0;
}
指针的运算
1.指针+-整数 :指针加上或减去整数时仍然是指针类型。指针加上整数会把指针向前移动一定距离,指针减去整数会把指针向后移动一定距离
2.指针-指针(运算的前提是两个指针指向了同一块空间):指针和指针之间的元素个数
注:无指针+指针
3.指针的关系运算
1)== ,!= :用于比较两个指针是否指向同一个内存地址
2)>,<,>=,<=用于比较两个指针所指向的内存地址的大小关系。比较的是指针指向的地址的值的大小
野指针
野指针的问题在于它指向的内存空间已经被释放,不再属于程序的所有权。这意味着针对这个指针进行的任何操作,如读取或写入,都是未定义的行为,可能导致程序崩溃或产生其他不可预测的结果。根据这个,引出它的成因和避免
1.成因
1)未初始化: int *p;
2) 越界访问:指针指向的范围超出数组范围时,p为野指针
3) 指针指向的内存空间释放:
#include <stdlib.h>
int main() {
int* ptr;
ptr = (int*) malloc(sizeof(int));
*ptr = 10;
free(ptr);
return 0;
}
2.如何规避野指针
1)指针初始化
2)小心指针越界
3)指针变量不再使⽤时,及时置NULL
4)避免返回局部变量的地址
assert断言
运行需要包含头文件assert.h,用于确保运行是否包含某条件 例如:assert(p!=NULL),若成立,继续运行。关闭时加上#define NDEBUG。
传值调用与传址调用
传值:把变量本⾝直接传递给了函数
传址:将变量的地址传递给了函数
注:
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
} //错误
============================================================
void Swap(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
} //正确
数组名
1)数组名就是数组⾸元素(第⼀个元素)的地址
2)sizeof(数组名),表示整个数组,计算的是整个数组的大小,单位是字节
3)&数组名,表示整个数组,取出的是整个数组的地址
4)整个数组的地址(+1操作是跳过整个数组的)和数组首元素的地址(+1跳一个元素)是有区别的,区别在哪里?就是在开头说的:这个地址指向的空间如何访问的。
一维传参的本质
一维数组传参的本质:传递的是数组首元素的地址
void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
二维数组传参本质
void test(int (*p)[5], int r, int c){ //包含5个整数的数组指针,表示二维数组的指针
int i = 0;
int j = 0;
for(i=0; i<r; i++) {
for(j=0; j<c; j++) {
printf("%d ", *(*(p+i)+j));//*(p+i)表示第i行的地址,*(*(p+i)+j)表示第i行第j列元素的值
}
printf("\n");
}
}
一些其他概念
二级指针
指针数组
int arr1[] = {1,1,1,1,1};
int arr2[] = {2,2,2,2,2};
int arr3[] = {3,3,3,3,3};
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
字符指针变量
const char* pstr = "hello world!"; // 把字符串 hello world!⾸字符的地址放到了pstr中
char str1[] = "hello bit.";
char str2[] = "hello bit.";
//数组名str1和str2在使用时会被编译器转换为一个指向数组第一个元素的指针,str1和str2实际上指向
//了两个不同的地址
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
//两个指向字符常量的指针变量,它们所指向的字符常量相同,因此它们的地址也相同。
数组指针变量
int(*p)[10] = &arr; //存放个数组的地址,就得存放在数组指针变量
函数指针
用来存放函数的地址,函数名就是函数的地址,也可以通过&函数名的方式获得函数的地址。
int (*p) (int x, int y)
函数指针数组
int (*parr1[3])();
sizeof和strlen的对比
1.sizeof是操作符
2.sizeof只关注占用内存空间的大小,不在乎内存中存放什么数据。
回调函数
将一个函数作为参数传递给另一个函数,并在需要的时候调用它。
其他的总结:
int *p1[3]; p1 连续空间的首地址标识,空间有六个元素,每个元素都是地址,int 方式进行访问
int (*p2)[6] p2 一把钥匙 连续空间访问内存,6个int6个int的访问
int *p3(double a) p3 升级为函数,输入参数double,返回类型是个地址
int (*p4)(double a) p4 一把钥匙,函数调用方式访问内存
p4 是一个指针,指向一个接受一个double类型参数并返回int类型值的函数
int (*p5[5])(double a) p5 钥匙串,函数调用方式访问
p5 是一个包含5个元素的数组,每个元素都是一个指向一个接受一个double类型参数并返回int类型值的函数的指针
补充
数组不是数据类型
为了表示多个同类型的连续空间,构造了一个数据结构的语法糖
数组名的含义
1.实际是常量指针
2.具有特殊的sizeof效果
3.数组名的地址是非法的,但被编译器重新定义了