什么叫指针
简单的理解:内存单元的编号==地址==指针
在指针中,&(取地址操作符)和*(解引用操作符)往往是形影不离的
int main()
{
int a = 10;
int* pa = &a;
printf("%d", *pa);
}
注意:在存放地址的时候必须要用指针来存放,下面我们就引入指针的概念:
int a = 10;
int * pa = &a;
上述这段代码表示pa的类型是int*,*说明pa是一个指针变量,而前面的int说明pa指向的是int类型的对象。a的内容是10,而pa指向的是a的地址,也就是可以根据a的地址去找到a的内容
什么叫解引用操作符(*)
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
上述代码就可以看到解引用操作符:*pa的意思就是通过pa存放的地址,找到指向的空间,*pa其实就是变量a;所以*pa = 0,就是把a的值改成了0
指针变量的大小
众所周知,指针变量的大小取决于地址的大小
32位平台下地址是32个比特(bit)位(4个字节)
64为平台下地址是64个比特(bit)位(8个字节)
注意指针变量的大小和类型是无关的,只要指针变量的类型在相同的平台下,大小都是相同的,举个例子:
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
x86也就是32位平台,x64也就是64位平台
指针变量类型的意义
指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀样的,为什么还要有各种各样的指针类型呢?
其实指针类型是有特殊意义的,我们接下来继续学习。
指针的解引⽤
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
根据上述代码我们可以看到,第一段代码会将n的4个字节全部改成0,而第二段代码则只会将n的第一个字节改成0
结论:指针的类型决定了指针解引用的时候一次能够操作几个字节
比如char*的指针解引用只能访问1个字节,而int*的指针解引用则能访问4个字节
指针+-整数
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跳过4个字节。这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后走一步有多大距离
void*指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进⾏指针的+-整数和解引⽤的运算(ps:具体内容会在后面讲函数参数部分体现)
const
const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤
int main()
{
int m = 0;
m = 20;
//m是可以修改的
const int n = 0;
n = 20;
//n是不能被修改的
return 0;
}
但是我们绕过n,使用n的地址,去修改n就能做到了,虽然这样做是在打破语法规则
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
那么我们如何防止这种事情发生呢?请看下面4段代码:
int main()
{
int n = 10;
int m = 20;
int *p = &n;
*p = 20;
p = &m;
}
int main()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;
}
int main()
{
int n = 10;
int m = 20;
int *const p = &n;
*p = 20;
p = &m;
}
int main()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20;
p = &m;
}
上述代码分别是不加const修饰,在*左边加const修饰,在*右边加const修饰 和在*左右两边修饰
结论:const修饰指针变量的时候
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。
指针运算
指针的基本运算有三种,分别是:
• 指针+-整数
• 指针-指针
• 指针的关系运算
指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
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++)
{
printf("%d ", *(p+i));//这里的p+i就是指针加减整数
}
return 0;
}
指针+-指针
指针+-指针得到的绝对值是指针和指针之间的元素个数
我们也可以将指针+-指针看做地址+-地址
注意:指针+-指针的前提必须是两个指着指向同一空间
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d", &arr[9] - &arr[0]);
return 0;
}
指针的关系运算
指针的关系运算就是指针的大小比较等:
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]);
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针的成因
1.指针未初始化
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <=11; i++)
{
*(p++) = i;
}
return 0;
}
这里arr只有10个元素,而for循环循环了11次,从而造成了越界访问,当指针指向的范围超出了数组arr的范围时,p就是野指针
3.指针指向的空间释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("abcdef\n");
printf("%d\n", *p);
return 0;
}
由于n是在test函数里面定义的,是局部变量,在test函数里面有一块单独的空间,而出了test函数指向的就不在是原来那块空间了,所以返回n的地址的时候就会有问题,所以这里打印p的值的时候是随机值
所以我们在返回的时候的时候千万不要返回局部变量的地址
assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣
任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误
流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
如果我们在确认代码没问题的话,我们无须再做断言,那么我们就在#include <assert.h>语句前面,定义一个宏NDEBUG
#define NDEBUG
#include <assert.h>
注意,assert一般是在debug版本使用,如果是在release版本使用的话会直接优化掉
指针的使用和传址调用
strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数
int my_strlen(const char* str)
{
int count = 0;
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d", len);
return 0;
}
传值调用和传址调用
首先,让我们看一下下面这段代码
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
很明显,交换失败了,那么是为什么呢?让我们一起来一探究竟吧
首先在Swap1函数 里面的x,y有着自己的地址,出了Swap1这个函数,这个空间就返还给了操作空间,所以就无法进行交换,我们将这种称为传值调用
结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不会影响实参
那么我们该这么办呢?
我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数Swap函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。
数组名的理解:
普通数组名
你知道吗?其实数组名就是地址,而且是首元素的地址,什么你说你不信,那么让我们继续往下看
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
这里我们使用&arr[0]拿到数组第一个元素的地址,但是其实数组名本来就是地址,而且是首元素的地址,让我们再来写一段代码测试一下吧
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中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
&数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除了上述两种情况,其他任何地方使用数组名,表示的都是数组首元素的地址
使用指针访问数组
有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
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;
}
在这里呢,数组名arr是首元素地址,可以赋值给p,其实数组名arr和p在这里是等价的
一维数组传参的本质
我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给⼀个函
数后,函数内部求数组的元素个数吗?请看下面这段代码
void test(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) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函
数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
二级指针
一般来说,有两颗*表示的就是二级指针,那么根据这个道理,三颗*表示的就是三级指针
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
**ppa首先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到a
**ppa的意思:第一颗*表示ppa是int*的类型,第二颗*表示ppa是个指针,指向的是pa的地址
指针数组
指针数组是指针还是数组呢?
我们类比一下,整形数组,是存放整形的数组,字符数组是存放字符的数组。
那么指针数组呢?肯定是存放指针数组的啦!
指针数组的每个元素都是用来存放地址(指针)的,如下如所示:
指针变量
字符指着变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char*
int main()
{
char ch = "w";
char* pc = &ch;
*pc = "k";
return 0;
}
当然,除了单个的字符写法还有另一种,那么就是字符串的写法啦
int main()
{
char* ptr = "hello world.";
printf("%s\n", ptr);
return 0;
}
代码char* ptr = "hello world.";特别容易误以为是把字符串hello world.放到字符指针ptr里面去,但是本质是把字符串hello world.首字符的地址放到了ptr中
数组指针变量
数组指针变量究竟是数组还是指针变量呢?
答:是指针变量
- 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
- 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
那么问题来了,下面哪个是数组指针变量呢?
int *p1[10];
int (*p2)[10];
答:int (*p2)[10]是数组指针变量
解释:p2先和*结合 ,说明p2是一个指针变量,然后指针指向的是一个大小为10个整形的数组。所以p2是一个指针,指向一个数组,叫数组指针。
这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p2先和*结合
数组指针变量初始化
数组指针变量是用来存放地址的,那怎么获取数组的地址呢?就需要我们之前学习的&数组名了
int arr[10] = {0};
&arr;//得到的就是数组的地址
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int (*p)[10] = &arr;
数组指针类型解析:
二维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
void test(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; 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()
{
printf("hehe\n");
}
int main()
{
printf("test = %p\n", test);
printf("&test = %p\n", &test);
return 0;
}
运行结果如下:
由上可见,函数确确实实是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址。如果要将函数的地址存放起来,那么我们就得创建函数指针变量咯,函数指针变量 的写法其实和数组指针的写法非常类似,写法如下:
void test()
{
printf("hehe\n");
}
void(*p1)() = test;
void(*p2)() = &test;
int Add(int x, int y)
{
return x + y;
}
int(*p3)(int, int) = Add;
int(*p4)(int x, int y) = &Add;//这里形参名x,y可以省略
函数指针类型解析:
函数指针变量去掉函数名剩下的就是他的类型啦!
typedef关键字
typedef是用来类型重命名的,可以将复杂的类型简单化。是不是有点难懂,不妨我们举个例子吧!
例如:unsigned int这个类型我们可以写成uint,既容易理解又方便,那么怎么实现呢?请看下面这段代码
typedef unsigned int uint
如果是指针类型的又如何重命名呢?其实也是可以的,比如将int*重命名为ptr_t
typedef int* ptr_t
但是对于数组指针和函数指针稍微有些不同,比如将数组指针类型int (*)[5]重命名为ptr_t,可以这样写:
typedef int(*ptr_t)[5]
新的类型名必须写在*的右边而不能直接写在最右,同理,函数指针类型的也一样
函数指针数组
在之前我们学习了指针数组,比如:
int* arr[10]//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
提问:下面三个代码哪个是函数指针数组?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答:parr1是函数指针数组
因为parr1先和[ ]结合,说明parr1是数组,那么数组的内容是什么呢?
是int(*)()类型的函数指针
sizeof和strlen的对比
sizeof
首先sizeof是计算变量中所占内存空间大小的,单位是字节,如果操作数是类型的话,计算的是类型创建变量所占空间的大小。
注意:sizeof只关注内存空间的大小,不在乎内存中存放的什么数据,比如下面这段代码:
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}
我们不仅可以根据变量名来计算,还可以通过类型计算,不过要注意的是利用类型计算的时候不能省略左右两边的()
strlen
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
}
strlen和sizeof的对比
sizeof | strlen |
1. sizeof是操作符 2. sizeof计算操作数所占内存的⼤⼩,单位是字节 3. 不关注内存中存放什么数据 | 1.strlen是库函数,需要使用头文件string.h 2.strlen是求字符串长度的,统计的是\0之前字符的个数 3.关注内存中是否有\0,如果没有\0,就会持续往后找,可能会越界 |