一、基本概念
1、内存和地址:
内存:内存单元的编号=地址=指针(C语言中给地址起的名字)
2、指针变量和地址:
2.1、取地址操作符&
2.2、指针变量和解引用操作符(*)
(1)、指针变量:存放地址
存放在指针变量中的值都会理解为地址
#iclude <stdio.h>
int main()
{
int a=10;
int* pa=&a; #取出a的地址并且存储到指针变量pa中
return 0;
}
(2)拆解指针类型
上面的代码中,*是在说明pa是指针变量,而前面的int是在说明pa指向的是整型类型的对象。
(3)解引用操作符
#include <stdio.h>
int main()
{
int a=100;
int* pa=&a;
*pa=0;
return 0;
}
*pa的意思是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了,所以*pa=0,这个操作符是把a改成了0
2.3、指针变量的大小:
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3、指针变量类型的意义
3.1、指针的解引用:
指针的类型决定了对指针解引用的时候有多大权限(一次能操作几个字节)
指针类型决定了指针进行+1,-1的时候,一次走远的距离
int* +1 走4个字节(整型的大小)
char* +1 走1个字节的大小(字符的大小)
#include <stdio.h>
int main()
{
int a=10;
int* pa=&a;
char* pc=&a;
printf("pa =%p\n",pa);
printf("pa+1=%p\n",pa+1);
printf("pc =%p\n",pc);
printf("pc+1=%p\n",pc);
return 0;
}
3.2指针+-整数
#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;
}
代码运行结果如下:
从中我们可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针变量+1跳过四个字节
3.3、void*指针
void*类型的指针可以理解为无具体类型的指针,这种类型的指针可以用来接受任意类型地址,但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用。
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据
4、const修饰指针
4.1、const修饰变量
int m=0;
m=20; //m可以修改
const int n=0;
n=20; //n不可以修改
4.2、const修饰指针变量
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。
const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
例如下面的代码:
#include <stdio.h>
void test1()
{
int n=10;
int m=20;
int *p=&n;
*p=20;
p=&m;
}
void test2()
{
int n=10;
int m=20;
const int* p=&n;
*p=20; //no
p=&m; //ok
}
void test3()
{
int n=10;
int m=20;
int *const p=&n;
*p=20; //ok
p=&m; //no
}
void test4()
{
int n=10;
int m=20;
int const * const p=&n;
*p=20; //no
p=&m; //no
}
int main()
{
test1();
test2();
test3();
test4();
return 0;
}
5、指针运算
5.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)就相当于*p,p++
}
return 0;
}
5.2、指针-指针
指针1 + 整数=指针2
指针2 - 指针1=整数
计算的前提条件是:两个指针指向了同一块空间
int main()
{
int arr[10] = { 0 };
char ch[5] = {0};
printf("%d\n", &ch[4] - &arr[6]);//err
return 0;
}
以上代码就会发生错误,char 类型和int 类型不同
#include <stdio.h>
#include <string.h>
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
str++;
return str - start;//指针-指针
}
int main()
{
//strlen - 求字符串长度的, strlen统计的是字符串中\0之前的字符个数
char arr[] = "abcdef";
//[a b c d e f \0]
int len = my_strlen(arr);//数组名arr是数组首元素的地址 arr == &arr[0]
printf("%d\n", len);
return 0;
}
代码运行结果如下:
5.3、指针的关系运算
指针和指针比较大小
地址和地址比较大小
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//10
int* p = arr;//&arr[0]
while (p < arr + sz) //指针的大小比较
{
printf("%d ", *p);
p++;
}
return 0;
}
6、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1、野指针成因:
指针未初始化
指针越界访问
指针指向的空间释放
6.2、如何规避野指针
(1)指针初始化:
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给NULL
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int*p = arr;//p此时不是野指针
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i <= sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
(2)小心指针越界
(3)指针变量不再使用时,及时置NULL,指针使用之前检查有效性
#include <stdio.h>
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int *p=&arr[0];
for(int i=0;i<10;i++)
{
*(p++)=i;
}//此时p已经越界了,可以把p置为NULL
p=NULL;
//下次使用的时候,判断p不为NULL的时候在使用
p=&arr[0] //重新让p获得地址
if(p !=NULL)
{
//...
}
return 0;
}
(4)避免返回局部变量的地址
7、assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止
运行,这个宏常常被称作“断言”。
assert(p!=NULL);
8、指针的使用和传址调用
8.1、strlen的模拟实现
求字符串长度,统计的是字符串中\0之前的字符的个数
//函数原型:
size_t strlen (const char * str)
// size_t 无符号的整型
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
8.2、传值调用和传址调用
例如:写一个函数,交换两个整型变量的值
我们可能会写出下面这种代码:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
//当实参传递给形参的时候,形参是实参一份临时拷贝
//对形参的修改不会影响实参的
Swap1(a, b); //传值调用
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
当我们运行结果时:a,b的值并没有发生改变
原因如下:在main函数内部,创建了a,b,在调用Swap1函数时,将a,b传递给了Swap1函数,在Swap1函数内部创建了形参x,y接受a,b的值,但是x,y的地址分别和a,b的地址不一样,相当于x和y是独立的空间,那么在Swap1函数内部交换x,y的值,自然不会影响a,b,Swap1函数在使用的时候,是把变量本身直接传给了函数,这种叫做传值函数
那如果使用指针,在main函数中将a,b的地址传递给Swap函数,Swap函数里面通过地址间接的操作main函数中的a,b,并交换就好了
#include <stdio.h>
void Swap2(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa;//tmp = a
*pa = *pb;//a = b
*pb = tmp;//b = tmp
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b); //传址调用
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
运行结果:
这里调用Swap2函数的时候时将变量的地址传递给了函数,这种函数调用方式叫传址调用。
二、数组与指针
1、数组名理解
数组名是数组首元素的地址,但有两个例外:
(1)、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
(2)、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
我们来看一段代码:
#include <stdio.h>
int main()
{
int arr[10]={0};
printf("arr =%p\n",arr);
printf("arr+1 =%p\n",arr+1);
printf("&arr[0] =%p\n",&arr[0]);
printf("&arr[0]+1=%p\n",&arr[0]+1);
printf("&arr =%p\n",&arr);
printf("&arr+1 =%p\n",&arr+1);
return 0;
}
打印结果如下:
在这里可以发现&arr[0] 和&arr[0]+1相差4个字节,arr 和arr+1相差4个字节,因为&arr[0]和arr都是首元素的地址,+1跳过了一个元素
但是&arr 和&arr+1相差40个字节,因为&arr是数组的地址,+1是跳过了整个数组的
2、使用指针访问数组
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
//使用指针来访问数组
int sz = sizeof(arr) / sizeof(arr[0]);
//输入10个值
int* p = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
//输入1个值
scanf("%d", p+i);//p+i == &arr[i]
}
//输出10个值
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));//p[i] arr[i] *(arr+i)//这里的p等价于arr
}
return 0;
}
3、一维数组传参的本质
数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参访问的数组是同一个数组的,函数形参的部分理论上应该是使用指针变量来接收首元素的地址,在函数内部sizeof(arr)计算的是一个地址的大小,而不是数组的大小,正是因为函数的参数部分的本质是指针,所以函数内部是没办法求数组元素个数的
//数组传参的时候,形参是可以写成数组的形式的
//但是本质上还是指针变量
#include <stdio.h>
//void Print(int arr[10])//int* arr
//{
// int sz = sizeof(arr) / sizeof(arr[0]);//得不到元素个数的
// int i = 0;
// for (i = 0; i < sz; i++)
// {
// printf("%d ", arr[i]);
// }
//}
void Print(int* p,int sz) //应该是指针
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
Print(arr, sz);//arr 数组名就是数组首元素的地址
//sizeof(arr)
//&arr
return 0;
}
一维数组传参,形参的部分可以写成数组形式,也可以写成指针形式
4、冒泡排序
核心思想:相邻两个元素进行比较
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
//趟数
for (i = 0; i < sz - 1; i++)
{
//假设已经有序了
int flag = 1;
//一趟排序的过程
int j = 0;
for (j = 0; j < sz-1-i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//排序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
//打印
print_arr(arr, sz);
return 0;
}
5、二级指针
指针变量也是变量,是变量就有地址,那么指针变量的地址存放在二级指针
#include <stdio.h>
int main()
{
int a=10;
int* p=&a; //p就是一级指针
int ** pp=&p; //pp是二级指针
return 0;
}
//pp的类型是int**--是二级指针类型
二级指针的运算:
*ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa
int b=20;
*ppa=&b;// 等价于 pa=&b
**ppa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,那找到的就是a
**ppa=30;
//等价于*pa=30;
//等价于a=30;
6、指针数组
整型数组--存放整型的数组
字符数组--存放字符的数组
指针数组--存放指针的数组
7、指针数组模拟二维数组
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素地址,类型是int*类型的,所以可以存放在arr数组中
int* arr[] = { arr1, arr2, arr3 };
//打印数组
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
arr[i]是访问arr数组的元素,arr[i]找到的数组元素指向了整型的一维数组,arr[i][j]就是整型一维数组中的元素
三、指针变量详解
1、字符指针变量
#include <stdio.h>
int main()
{
const char* pstr="hello.bit.";
printf("%s\n",pstr);
return 0;
}
代码const char* pstr="hello bit.";特别容易让我们以为是把字符串放在了字符指针pstr里面了,但是本质是把字符串的首字符的地址存放到了pstr中
char str1[]="hello bit.";
char str2[]="hello bit."
const char *str3="hello bit.";
const char *str4="hello bit.";
当几个指针指向同一个字符串的时候,他们实际上会指向同一块内存,但是用相同的常量字符串去初始化不同的数组时就会开辟出不同的内存块,所以str1和str2不同,str3和str4相同
2、数组指针变量
2.1、数组指针是指向数组的指针
指针数组是存放指针的数组
int* p1[10];//p1是指针数组,存放指针的数组
int (*p2)[10];//p2是指针变量,指向的是数组
[ ]的优先级要高于*号,所以必须加上( )保证p先与*结合
2.2、数组指针变量怎么初始化
获得数组的地址使用 &数组名,如果要存放整个数组的地址,就得存放在数组指针变量中
#include <stdio.h>
int main()
{
int arr[10] = {0};
int* p1 = arr;
//int* int*
printf("%p\n", p1);
printf("%p\n", p1+1);
int (*p2)[10] = &arr;
//int (*)[10] int(*)[10]
printf("%p\n", p2);//&arr
printf("%p\n", p2+1);//&arr+1
return 0;
}
&arr和p的类型是完全一致的
3、二维数组传参的本质
二维数组的数组名表示的就是第一行的地址,是一维数组的地址,那就意味着二维数组传参本质上也是传递了地址,传递的是第一行一维数组的地址
#include <stdio.h>
void print(int (*arr)[5], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(arr + i) + j));//arr[i][j]
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6}, {3,4,5,6,7} };
//数组名是数组首元素的地址
//例外:1. &数组名
// 2. sizeof(数组名)
//
print(arr, 3, 5);
return 0;
}
二维数组传参,形参的部分可以写成数组,也可以写成指针形式
4、函数指针变量
4.1、函数指针变量的创建
函数指针变量应该是存放函数地址的,未来通过地址能够调用函数的,函数名就是函数的地址,当然也可以通过&函数名来获得函数的地址,如果要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法和数组指针非常类似
4.2、函数指针变量的使用
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (* pf)(int,int) = Add;
int c = Add(2, 3);//函数名调用
printf("%d\n", c);
int d = (*pf)(3, 4);//函数指针调用
printf("%d\n", d);
int e = pf(4, 5);//函数指针调用//pf是专门用来存放函数的地址的,pf就是函数指针变量
printf("%d\n", e);
return 0;
}
5、函数指针数组
int (*prr[3])();
prr先和[ ]结合,说明prr是数组,数组的内容是int (*)()类型的函数指针
ps:大家点点赞,作者接着更~