梦开始的地方 —— C语言指针进阶
指针进阶
0.前言
从上一篇博客指针入门我们知道了,几点
- 指针是一个变量,存放的是地址,地址是某一块内存空间的唯一标识
- 指针的大小在不同平台的大小不一样,32位平台的是4个字节,64位平台是8个字节
- 指针的类型决定了指针解引用和指针加减能走多远
1. const修饰指针
我们知道const修饰一个变量,它就是一个常变量是不会被直接修改的。
#include <stdio.h>
int main()
{
const int num = 10;
num = 10; //这里会报错
return 0;
}
当我们可以通过指针来进行修改,也就是不直接对它进行修改,而是取出它的地址通过地址来对它进行修改。
#include <stdio.h>
int main()
{
const int num = 10;
int* p = #
*p = 20; //num被修改成了20
printf("%d\n", num);
return 0;
}
那么const的就失去了原本的意义,其实还有一种另外的方式可以达到我们想要的效果。那就是const修饰指针
代码示例:如果再尝试使用地址的方式修改变量编译器就会报错。
#include <stdio.h>
int main()
{
int num = 10;
const int* p = #
//int const* p = # 这两个写法没区别
*p = 20;
printf("%d\n", num);
return 0;
}
那么问题又来了,我们不能通过地址修改但此时又可以通过直接修改的方式进行修改
#include <stdio.h>
int main()
{
int num = 10;
const int* p = #
num = 20;// num被修改成了20
printf("%d\n", num);
return 0;
}
依旧是可以解决的,我们在用一个const修饰一下 p就好了,再用const修饰变量,此时无论是直接修改还是通过取地址都无法修改num的值了,那const到底怎么修饰指针呢?
#include <stdio.h>
int main()
{
const int num = 10;//让num不能通过变量名修改
const int* const p = #//不允许通过*修改num的地址,同时指针变量p也不能被改变
printf("%d\n", num);
return 0;
}
总结:
-
const写在星号
*
左边,修饰的是指针指向的内容,使得指针指向的内容,不能通过指针来改变。但是指针变量本身是可以修改的。#include <stdio.h> int main() { int num = 10; int n = 100; const int* p = # *p = 20;//错误写法 p = &n; //正确写法 num = 200;//正确写法 return 0; }
-
const写在星号
*
右边,修饰的是指针变量本身,使得指针变量本身不能修改。但是指针指向的内容可以通过指针来改变,是可以修改的。#include <stdio.h> int main() { int num = 10; int n = 100; int* const p = # *p = 20;//正确写法 p = &n; //错误写法 num = 200; // 正确写法 return 0; }
2. 字符指针
在C语言中字符指针用char*
来表示
#include <stdio.h>
int main()
{
char c = 'Q';
char* cp = &c;
printf("%c\n", *p);
return 0;
}
而char*
还有另外一种用途,那就是常量字符串
#include <stdio.h>
int main()
{
char* str = "hello";// 常量字符串
char arr[] = "hello";
printf("%c\n", *str);
printf("%s\n", str);
printf("%s\n", arr);
return 0;
}
str 里存放的是“hello”字符串的首地址,且这是一个常量字符串,也就是它不能被修改
而arr是一个字符数组它是可以被修改的
再来看一段代码
#include <stdio.h>
int main()
{
char* str1 = "hello";
char* str2 = "hello";
char arr1[] = "hello";
char arr2[] = "hello";
if (str1 == str2)
{
printf("str1 == str2\n");
}
else
{
printf("str1 != str2\n");
}
if (arr1 == arr2)
{
printf("arr1 == arr2\n");
}
else
{
printf("arr1 != arr2\n");
}
return 0;
}
运行结果
str1 == str2
arr1 != arr2
str1 == str2 是因为,"hello"是一个常量字符串,常量字符串是不能被修改的。所以常量字符在内存中只会存一份,所以 str1和str2存放的都是 “hello”这个字符的首地址
而arr1 和 arr2 是数组名,数组名存储放的是首元素的地址,这是两个不同的数字当然不相等。
注意:比较字符串相等要使用 strcmp
2. 指针数组
整形数组、字符数组都是数组,那么指针数组也是数组,它是一个存放指针的数组。上一篇博客也提到过
基本语法
#include <stdio.h>
int main()
{
int* arr[10];//整形指针数组
char* ch[10]; //字符指针数组
int** arrP[10]; //二级整形指针数组
int a = 10;
char c = 'a';
int* p = &a;
arr[0] = &a;
ch[0] = &c;
arrP[0] = &p;
printf("%d\n",*arr[0]);
printf("%c\n",*ch[0]);
printf("%d\n",*(*arrP[0]));//第一次解引用拿到一级指针p的地址,再次解引用就拿到了a的地址
return 0;
}
3. 数组指针
1) 数组指针概念
数组指针是指针还是指针?
我们知道整形指针int *
指向的是一个整形变量,也知道字符指针char*
指向的是一个字符变量,那么数组指针它也是一个指针,它是一个指向数组的指针。
那么下面两个哪个才是数组指针呢?
int *arrP[10];
int (*pArr)[10];
int *arrP[10]
,因为[]
的优先级要高于*
,所以arrP会和[]
先结合变成arrP[10]
这就是一个数组,那么int就会和*
结合变成int *
,那么此时就变成了一个int*
类型的数组,也就是整形指针数组- 给
(*pArr)
括起来,那么它们结合就是一个指针,指向了一个数组[10]
,所以这里可以描述为,*pArr
指向了一个10个元素的数组,每个元素的类型是int,所以int (*pArr)[10]
是一个数组指针
2) 数组名 和 &数组名
我们给一个数组
int arr[10]
这个数组的arr
和&arr
分别是啥?
来看一段代码
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
打印结果:发现arr和&arr的地址是一样的?我们知道arr存的是数组首元素的地址,那 &arr也是吗?并不是
00000030B82FFCF8
00000030B82FFCF8
再来看一段代码
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr);
printf("%p\n", &arr);
printf("==========\n");
printf("%p\n", arr+1);
printf("%p\n", &arr+1);
return 0;
}
打印结果
000000FA108FF608
000000FA108FF608
==========
000000FA108FF60C
000000FA108FF630
我们发现,arr
和&arr
打印的地址是一样的。但是它们的意义是完全不一样的。
arr+1
只是跳过了4个字节的地址,而&arr+1
则是条过了40个字节的地址,相当于跳过了整个数组。
从这里看出来&arr
取出的是整个数组的地址!
注意:
- &数组名
- sizeof(数组名)
- 除此上面2种情况之外-所有遇到的数组名都是数组首元素的地址
3) 数组指针的使用
了解了数组指针,那么数组指针是怎么使用的呢?
整形指针存的是整形的地址,那么数组指针指向的是一个数组,那它存的就是数组的地址。
前面我们了解到通过指针±可以遍历数组,指针p存的是数组首元素的地址,那么怎么通过数组指针遍历数组呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", *(p+i));
}
return 0;
}
数组指针的使用,通过使用数组指针,用三种不同的方式遍历整个数组(注意:一下写法一般不会使用)
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*pArr)[10] = &arr;
int i = 0;
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", (*pArr)[i]);
}
printf("\n");
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", *(*pArr+i));
}
printf("\n");
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", pArr[0][i]);
//等价于
//printf("%d ", *(*(p) + i))
}
return 0;
}
(*pArr)
拿到的是整个数组的地址,而整个数组的地址是有arr来维护的,其实*pArr[0]
等价于arr[0],也就可以理解为*pArr
<==> arr- 第二种写法:
*pArr
等价于arr,那么其实它就是首元素的地址,那么*pArr+i
其实拿到的是下标为 i 元素的地址,再对其解引用拿到的就是这个元素 - 第三种写法:
pArr[0]
其实等价于*(pArr+0)
,也就是说pArr[0][i]
<==>*(*(p+0) + i))
上面的那些写法让人决得很变扭,一般不会这么写。
来看看正常的数组指针的使用,一般是用于二维数组。
我们通过函数来打印数组正常传参,传过去的是二维数组就通过二维数组来接收。其实还可以通过数组指针来接收。
#include <stdio.h>
void prt1(int arr[3][4], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void prt2(int (*p)[4], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
prt1(arr, 3, 4);
prt2(arr, 3, 4);
return 0;
}
数组名是首元素地址,而二维数组的数组名则是二维数组第一行的地址。
那么*(*(p+i)+j)
是什么意思呢?
- p 是第一行的地址
p+i
是跳过i行后的那一行的地址*(p+i)
跳过i行之后的那一行,也就相当于这一行的数组名(*(p+i)+j)
跳过i行后的那一行的下标为j的元素地址*(*(p+i)+j)
跳过i行后的那一行的小标为j的元素
需要注意的是,这里传递过去的是数组数组名,也就是数组首元素的地址,也就是二维数组的第一行。
再来看一段代码
#include <stdio.h>
int main()
{
int arr[3][4] = { 0 };
int (*p1)[4] = arr;//二维数组首元素,也就是第一行的地址
int (*p2)[3][4] = &arr;//这是整个二维数组的地址
printf("%p\n", p1);
printf("%p\n", p2);
printf("==============\n");
printf("%p\n", p1+1);
printf("%p\n", p2+1);
return 0;
}
运行结果:我们发现p1+1
只是跳过了一行一行4个元素也就是16个字节,而p2+1
跳过了整个二维数组,也就是48个字节。说明二维数组其实和一维数组,数组名和&数组名是一样的。
0000002D8135F4B8
0000002D8135F4B8
==============
0000002D8135F4C8
0000002D8135F4E8
4) 简单汇总
//这是普通数组
int arr[10];
// pArr1和[]结合这是一个数组,int*结合,所以这一个 指针数组
int *pArr1[10];
// *和pArr2结合,这是一个指针,指向的是一个存放10个元素的数组,数组的类型是Int。所以这是一个指针数组
int (*pArr2)[10];
// pArr3和[]先结合这是一个数组,它是存放数组指针的数组
int (*pArr3[10])[5];
解释一下最后一个数组
pArr3和[]结合他是一个数组,每个数组有10个元素
把数组名去掉,剩下的就是元素类型 int (*)[5],这是一个指针指向一个元素5个元素的数组,每个数组的元素类型是int
比如 int (*p)[5] = &arr; 这是一个数组指针,假设把p去掉 int(*)[5],剩下的就是它的元素类型了。
pArr3[10]是一个存放数组指针的数组,数组有10个元素
所以这是一个
int main()
{
int arr1[5] = { 0 };
int arr2[5] = { 0 };
int arr3[5] = { 0 };
int (*pArr3[10])[5] = { &arr1,&arr2,&arr3 };
return 0;
}
4. 数组和指针传参
1) 一维数组传参
#include <stdio.h>
//数组传参数组接收没问题
void test1(int arr[]){}
//数组传参数组接收写了元素类型也没问题(这本质是一个指针,写不写都一样)
void test1(int arr[10]){}
//传数组名,本质上就是首元素的地址,没问题
void test1(int* arr){}
//传指针数组,指针数组接收每问题
void test2(int* arr[20]){}
//指针数组的数组名本质上也是一个地址,数组的每个元素都是int* 类型,那它的首元素就是一个一级指针,用一个二级指针接收一级指针每有任何问题
void test2(int** arr){}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test1(arr);
test2(arr2);
}
2) 二维数组传参
#include <stdio.h>
//传二维数组用二维数组接收每任何问题
void test(int arr[3][5]){}
//在C语言中二维数组的列是不能省略的!
void test(int arr[][]){}
//省略行每任何问题,省略行没问题,可以不知道有多少行,但必须知道每行有多少列,猜方便运算
void test(int arr[][5]){}
//二维数组传的是二维数组首元素也就是第一行的地址,拿一个int* 的指针来接收肯定是不行的。可以存放到数组指针去
void test(int* arr){}
//这是明显错误的,这么写表示这是个整形指针数组
void test(int* arr[5]){}
//这是正确的写法,这是一个指针指向了一个二维数组第一行的地址,数组每一个行有5个元素,
void test(int(*arr)[5]){}
//错误写法,这是一个二级指针,只能用来接收一个一级指针的地址
void test(int** arr){}
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
3) 一级指针传参
一个int*
的指针能接收什么参数?
#include <stdio.h>
void test(int *p)
{
}
int main()
{
int a = 10;
int *pa = &a;
int arr[10] = {0};
//下面三中写法都是正确的
test(&a);
test(pa);
test(arr);
return 0;
}
char*
又能接受什么参数?
char*
有两种表示形式,一种是字符指针,一种是字符串,存的是字符串的收地址。
#include <stdio.h>
void test(char *p)
{
}
int main()
{
char c = 'a';
char str[] = "hello";
char arr[] = {'h','e','l','l','o','\0'};
test(&c);
test(str);
test(arr);
return 0;
}
4) 二级指针传参
二级指针存放的是一级指针的地址,所以传递个一级指针的地址,或者二级指针都是可以的
#include <stdio.h>
void test(int** p)
{
printf("%d\n", **p);
}
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
test(&p);
test(pp);
return 0;
}