一.指针基础
1.指针定义的理解
就像我们住房子划分房间一样,在系统中,内存会被划分为多个内存单元,一个内存单元大小是一个字节,我们在搜索房间时有门牌号,与之类似,每个内存单元都会有一个编号 = 地址 = 指针,计算机中把内存的编号叫地址,C语言中给地址起了新名字指针
2.变量的创建和指针的指向
变量创建的本质是在内存中开辟一块空间
int main() {
int a = 10; //变量创建的本质是在内存中开辟一块空间
//分配了四个字节,取地址是第一个字节的地址(较小的)
printf("%p",&a); //&取地址操作符 %p专门打印地址的(十六进制的形式打印的)
//printf("%X", &a);
return 0;
}
在上面,我们定义了一个int类型的变量a,而当a创建时,就要为a分配空间,int型分配四个字节,我们使用&(取地址符号)获得的就是a的第一个字节(最小的字节)的地址
假设内存中这样存放a,我们取地址取出的就是0x0012ff40
3.指针的书写和含义
那么我们将a的地址取出来后,怎么把它放到一个变量里呢?
我们这样来书写:
int a = 10;
int* pa = &a;
int * pa中,*代表pa是指针变量,int代表该指针指向的类型是什么(a的类型)
那么,既然指针也是一个变量,那么指针变量的大小是多少?
4.指针变量的大小
指针变量需要多大的空间,取决于存放的是什么(地址),地址的存放需要多大的空间,指针变量的大小就是多大(与指针指向的内容的类型无关)
我们可以写出如下代码来观察指针的内存
int main() {
int a = 20;
char ch = 'w';
char* pc = &ch;
int* pa = &a;
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(int*));
return 0;
}
分开写结果如下:
5.指针类型的意义
我们已经知道了*代表这是一个指针变量,那么前面指向的类型又有什么作用呢?
事实上,指针类型决定了指针在进行解引用的时候访问多大的空间,例如,我们在进行解引用操作时,int*解引用访问4个字节,char*解引用访问1个字节
分析图如下:
当我们使用char *去指向int类型时,解引用只会访问一个字节的内容,而我们知道int类型是四个字节,所以出现了错误
6.指针相加减
指针类型决定了指针的步长,向前或者向后走一步大概的距离
int main() {
int a = 10;
int* pa = &a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
char* pc = &a;
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
对于int*类型的指针变量,+1会移动四个字节,char*类型的指针变量,+1会移动一个字节,因此,我们要根据实际的需要,选择适当的指针类型才能达到效果
7.void*指针
无具体类型的指针(范性指针)可以接受任意类型的地址,不能进行解引用的操作,和指针的+-操作
int main() {
int a = 10;
int* pa= &a;
void* pc = &a;
char b;
void* pd = &b;
return 0;
}
8.const和指针
我们知道,const表示常属性,不能被修改,例如:
int main() {
const int n = 10; //n是变量,但不可以改变,常变量
//如果不想让n变,就在前加const
printf("%d", n);
return 0;
}
此时,n是无法改变的
但是我们可以使用指针来更改数字,如下:
int main() {
const int n = 10;
int* p= &n;
*p = 20;
printf("%d", n);
return 0;
}
这样,我们就可以改变n的变量
const修饰指针有两种情况,分为const在*前和const在*后,
1.const放在*的左边,修饰的是*p const int* p
2.const放在*的右边,修饰的是p int* const p
两者有什么区别呢?
int main() {
const int n = 10;
int m = 100;
//const int(* p) = &n;
*p = 20; //不能改变对象的值了
// p = &m; //可以改变指向的对象
//int* const p = &n;
p = &m; //不可以改变指向的对象
//*p = 20; //可以改变对象的值
//p是指针变量,里面存放了其他变量的地址
//p是指针变量,*p(n)是指针指向的对象
//p是指针变量,p也有自己的地址&p
return 0;
}
const放在*前,会管整个(*p),使其不能改变值,但是p不受管理
const放在*后,只管p,不能改变指向的对象,但是可以改变*p
9.指针的运算
指针的基本运算分为三种:
指针+-整数
指针 - 指针
指针的关系运算
指针加减整数运算
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组在内存中连续存放,随着下标增长,地址由低到高变化的
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d\n", *p);
p++; //p = p + 1
}
return 0;
}
指针-指针 == 指针之间元素的个数(运算的前提条件是两个指针指向了同一块的空间)
int main() {
int arr[10] = { 0 };
printf("%d", &arr[9] - &arr[0]);
printf("%d", &arr[0] - &arr[9]);
//指针 - 指针的绝对值是指针和指针之间的元素个数
return 0;
}
指针的运算关系:两个指针比较大小
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
while (p < arr + sz) {
printf("%d\n", *p);
p++;
}
return 0;
}
10.野指针
野指针 指针指向的位置是不可知的(随机的,不正确的,没有限制的)很危险
野指针的成因:
//成因一:指针未初始化
int main()
{
int* p; //局部变量不初始化的时候,他的值是随机值
*p = 20;
printf("%d\n", *p);
return 0;
}
//成因二:指针的越界访问
int main() {
int arr[10] = { 0 };
int* p = arr[0];
int i = 0;
for (i = 0; i < 11; i++) { //指向了不属于的空间,且用指针访问了
*(p++) = i;
}
return 0;
}
//成因三:指针空间释放
int* test() {
int n = 100;
return &n;
}
int main() {
int* p = test(); //p存n的地址,但是空间回收了
printf("%d", *p);
return 0;
}
如何规避野指针
1.指针初始化 知道就直接赋地址,不知道就赋值NULL
2.小心指针越界
3.指针不在使用时,及时置为NULL,指针使用前检查有效性
4.避免返回局部变量的地址
int main()
{
//int a = 10;
//int* pa = &a;
//*pa = 20;
//printf("%d", a);
//int* p = NULL; //空指针,地址是0,不能赋值
//*p //err
int a = 10;
int* p = &a;
if (p != NULL) //检测指针有效性
{
*p = 20;
}
return 0;
}
那是否我们可以更简单的方法来检测指针的有效性呢?
11.assert断言
assert的作用:运行时确保程序符合指定条件,不合条件,报错并终止运行
形式如下 assert(表达式); 表达式如果不成立则会报错
当然,assert需要头文件
#include <assert.h>
上面的代码可以更简单的写成下面的形式
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL); //err
*p = 20;
printf("%d\n", *p);
return 0;
}
在这里,由于*p是NULL,直接报错并终止运行了
如果后面我们不再需要assert断言,我们不需要把所有写出的assert代码都手动删除,只需要在头文件位置写成:
#define NDEBUG
#include <assert.h>
即可使得assert失效
12.指针的使用和传址调用
(1)我们尝试用代码实现strlen()
size_t Mystrlen(const char* s)
{
size_t count = 0;
assert(s != NULL);
while (*s != '\0') {
s++;
count++;
}
return count;
}
int main()
{
//strlen()-求字符串长度
char arr[] = "abcdef";
size_t len = Mystrlen(arr); //字符串中\0前的字符个数
printf("%zd\n", len);
return 0;
}
(2)函数实现两个数的交换
void swap(int x, int y) {
int z = 0;
z = x;
x = y;
y = z;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(a, b);
printf("%d %d\n", a, b);
return 0;
}
我们写出代码发现a与b的值没有发生改变,实际上,实参变量传给形参时候,形参是一份临时拷贝,对形参的修改不会影响实参,因此,我们应该传地址过去,实现改变
void swap(int* x, int* y) {
int z = 0;
z = *x;
*x = *y;
*y = z;
return 0;
}
int main()
{
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(&a, &b); //传址调用
printf("%d %d\n", a, b);
return 0;
}
二.指针和数组
1.数组名的理解
在大部分情况下,数组名是数组首元素的地址,有两个例外:sizeof数组名,这里表示整个数组的大小,&数组名,这里的数组名也表示整个数组,取出整个数组地址
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", arr + 1); //跳过了一个元素
printf("%p\n", &arr[0] + 1); //跳过了一个元素
printf("%p\n", &arr + 1); //跳过了一个数组
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr[0]));
return 0;
}
2.指针表示数组
我们知道了,数组名实际上是数组第一个元素的地址,因此,指针表示数组可以写成
arr[i] == *(arr+i) == *(p + i) == p[i]
我们输出数组元素可以使用指针的表示:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//for (i=0; i < sz; i++) {
// scanf("%d", p + i);
//}
for (i=0; i<sz; i++) {
printf("%d ", *(p+i));
}
return 0;
}
3.一维数组传参的本质
一维数组传参传过去的是数组首元素的地址
void test(int arr[]) //本质上arr是指针
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz2);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz1);
test(arr);
return 0;
}
4.冒泡排序法
冒泡排序的思想
1.两两相邻的元素进行比较
2.如果不满足顺序就交换,满足顺序就找下一对
例如,现在给出我们一个数组,让我们将数组由小到大(升序)排列
过程大致如下
从头开始进行冒泡排序,一趟冒泡排序会排好最后一个的内容,我们如果需要排n个元素,最多需要进行n-1次,而且由于每次都可以排好一个,因此每次冒泡需要的次数越来越少
我们写出以下的代码实现了冒泡排序:
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]);
}
printf("\n");
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1 };
//实现排序,排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz);
print_arr(arr, sz);
return 0;
}
当然,我们所说的进行n-1次冒泡排序是我们考虑的最坏的结果,很多时候,在我们没有进行到n-1次时排序就已经完成,排序完成的标志是本次排序中没有进行过一次交换,因此,我们可以加入判断是否交换过来提前结束本次的排序
我们写出了如下函数:
void bubble_sort2(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
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;
}
}
}
二级指针
一级指针指向的是其他变量,而二级指针是存放一级指针地址的
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
**pp = 20;
printf("%d\n", a);
return 0;
}
5.指针数组
指针数组是存放指针的数组,是一个数组,数组内容是指针
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[3] = {arr1, arr2, arr3};
其中int * arr[3]就是一个指针数组
我们可以使用指针数组来模拟实现二维数组
这样使用了指针数组来实现了二维数组的访问
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[3] = {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;
}
三.字符指针,数组指针和函数指针
1.字符指针-存放字符的指针
字符指针是一个指针,指向的内容是字符/字符串
int main()
{
char ch = 'w';
char* pc = &ch;
printf("%c\n", *pc);
*pc = 'q';
printf("%c\n", ch);
return 0;
}
也可以拿字符指针指向字符串,代码如下
int main()
{
char* p = "hello world";
return 0;
}
但是,对于上面的代码,我们如果尝试修改指针的内容:
int main()
{
const char* p = "hello world";
* p = "hi world";
return 0;
}
发现会报错,实际上,我们可以把字符串理解成字符数组,但并不完全是,区别是:数组是可以修改的,而这里的p指向的是常量字符串,不能修改,但是可以使用
int main()
{
const char* p = "hello world";
printf("%s\n", p);
printf("%s\n", "hello world");
int len = strlen(p);
int i = 0;
for (i = 0; i < len; i++)
{
printf("%c",*(p + i));
}
return 0;
}
这样我们就可以使用字符指针输出字符串啦。
2.数组指针
数组指针是指向数组的指针,是指针,存放的是数组
int main()
{
int arr[10] = { 0 };
int (*p)[10] = &arr;//取出的是数组的地址 数组指针
// int *p[10]; 指针数组
return 0;
}
请注意数组指针和指针数组的区别,这里写出了一些代码:
int main()
{
char arr[5];
char (*p1)[5] = &arr; //char (*)[5]是数组指针类型
//5不能省略掉
char* p2 = arr;
char* p3 = &arr[0];
return 0;
}
char (*p1)[5] = &arr;定义了一个数组指针
char* p2 = arr;
char* p3 = &arr[0];定义的都是指针,指针指向的是arr数组
二维指针传参时候,二维数组的数组名是第一行的地址
我们在平时输出二维数组时,代码是这样的:
void test(int arr[3][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]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
我们可以利用数组指针来实现二维变量
void test_(int(*p)[5], int r, int c)
{
int x = 0;
for (x = 0; x < r; x++)
{
int y = 0;
for (y = 0; y < c; y++)
{
printf("%d ", (*(p+x))[y]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
test_(&arr, 3, 5);
return 0;
}
3.函数指针
函数指针是一个指针,指向的是函数,存放的是函数的地址
int Add(int a, int b)
{
return a + b;
}
int* test(char* s)
{
return NULL;
}
int main()
{
int (*pf)(int, int) = Add;
int* (*ph)(char* s) = test;
//int x = 10;
//int y = 20;
//int z = Add(x, y);
printf("%p\n", &Add);
printf("%p\n", Add);
//&函数名和函数名都表示函数的地址
return 0;
}
函数指针变量的写法和数组指针变量的写法类似
4.typedef重命名
typedef是用来类型重命名的,可以将复杂的类型简单化
typedef unsigned int uint;
int main()
{
unsigned int num1;
uint num2;
return 0;
}
在这里我们对指针类型进行重命名
typedef int* pint;
int main()
{
int i = 10;
int* p1 = &i;
pint p2 = &i;
return 0;
}
typedef和define的区别
我们知道两者都可以定义全局变量,两者的区别如下
typedef int* ptr_t;
#define PTR_T int*
int main()
{
ptr_t p1, p2;//p1,p2是整型指针
PTR_T p3, p4;//int *p3,p4
//p3是指针,p4是整型
}
5.函数指针数组
如果把多个相同类型的函数指针存放在一个数组中,这个数组就是函数指针数组
我们写出如下的函数:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
我们定义函数指针这样来定义,我们发现他们的输入类型和输出类型都是相同的
int main()
{
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
return 0;
}
因此,我们可以创建一个函数指针数组,代码如下:
int main()
{
int (*pfarr[4])(int, int) = {Add, Sub, Mul, Div};//pfArr就是函数指针
int i = 0;
for (i = 0; i < 4; i++)
{
int ret =pfarr[i](8, 4);
printf("%d\n", ret);
}
return 0;
}
这样我们就可以更简便的定义啦
如果让我们编写一个整型的加减乘除计算器,我们还可以写出一个函数
void Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d", ret);
}
void menu()
{
printf("**********************\n");
printf("*****1.add 2.sub*****\n");
printf("*****3.mul 4.div*****\n");
printf("*****4.exit***********\n");
printf("**********************\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入");
scanf("%d", &input);
switch(input)
{
case 1:
Calc(Add);
break;
case 2:
Calc(Sub);
break;
case 3:
Calc(Mul);
break;
case 4:
Calc(Div);
break;
case 0:
printf("退出计算器");
break;
default:
printf("选择错误");
break;
}
} while (input);
return 0;
}
这样就可以了
但我们发现,如果我们运用switch还是很麻烦,因此我们创建一个函数指针数组
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//创建一个函数指针的数组
int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
// 0 1 2 3 4
do
{
menu();
printf("请输入");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else if (input == 0)
{
printf("退出");
break;
}
else
{
printf("选择错误,重新选择");
}
} while (input);
return 0;
}
这样就可以使得代码更加的优秀了