目录
(1)在使用指针时要初始化指针,当不知道初始化为什么值的时候我们可以将他置为NULL。
(4)当指针使用完之后要及时置为NULL,使用之前要检查它的有效性。
一.内存和地址
1.内存
(1)内存:内存是用来存放机计算机CPU处理之前和处理之后的数据的。
(2)内存的管理
- 内存又被分为小单元格,每个小单元格的大小为一个字节。
- 为了方便对内存进行管理,我们对每个地址单元进行了编号,这样CPU就能通过编号快速找到一个内存空间。
2.地址(指针)
(1)这些内存单元的编号就被称为地址。在C语言中,我们将这些地址叫做指针。
(2)我们可以简单的理解为:内存单元编号 == 地址 == 指针
3.计算机的编址
计算机的地址编号并不是真实存在的,而是由计算机的硬件实现的。计算机的CPU和内存之间是由地址总线连接的,而这也是硬件编址的基础。32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。地址被下达给内存,在内存上就可以找到该地址对应的数据,将数据通过地址总线传入CPU。
二.指针变量和地址
1.取地址操作符(&)
取地址操作符是用来取出变量的地址的,因为每一个内存单元的大小是一个字节,&取出的是第一个字节的地址。
#inlcude <stdio.h>
int main()
{
int a = 10;
printf("%p\n",&a);
return 0;
}
调试结果:
输出结果:
在上图中我们可以看见a的地址和它下面的地址相差4,因为a是整型,所以a占的字节是4个字节,程序打印的是第一个字节的地址。因为,当我们找到数据的第一个字节后,我们就可以根据他的数据类型找到他剩余的地址。
2.解引用操作符(*)和指针变量
(1)指针变量:指针变量是用来存放指针的地址的,通常与*连用。指针变量也是变量,存放在指针变量中的值都会被理解为地址。
#include <stdio.h>
int main()
{
int a = 10;
int *pa = &a;//取出a的地址放在pa中
return 0;
}
(2)指针变量各部分的理解
pa是指针变量的变量名;*说明pa是指针变量,int说明pa指向的数据是int类型(整型)。
(3)解引用操作符
*解引用操作符可以通过指针变量中存放的地址找到所指向的空间,并对其中的值进行修改。
#include <stdio.h>
int main()
{
int a = 25;
int *pa = &a;
*pa = 10;
printf("%d\n",a);
retrun 0;
}
输出结果:
*pa的意思就是通过a的地址找到a变量的内存空间,当指针变量要通过它存放的变量的地址访问该变量时需要用*(解引用操作符)
(4)指针变量的大小
指针变量在不同的环境下有所不同:
- 在32位环境下,指针变量的大小为4个字节
- 在64位环境下,指针变量的大小为8个字节
#include <stdio.h>
int main()
{
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(float*));
printf("%zd\n", sizeof(double*));
return 0;
}
输出结果:
结论:
- 32位平台下的地址是32个bit位,指针变量的大小在32位环境下为4个字节
- 64位平台下的地址是64个bit位,指针变量的大小在64位环境下是8个字节
- 指针变量的大小和类型无关,在同一平台下指针变量的大小是相同的。
三.指针变量类型的意义
1.指针的解引用
指针变量的类型决定了,对指针解引用时有多大的权限。
代码1:
//代码一
#include <stdio.h>
int main()
{
int a = 0x11223344;
int *pa = &a;
*pa = 0;
return 0;
}
调试结果如下:
代码2:
//代码二
#include <stdio.h>
int main()
{
int a = 0x00789010;
char* pa = (char*)&a;
*pa = 0;
return 0;
}
调试结果如下:
通过对比代码一和代码二,我们不难发现代码1将a的四个字节全部改为了0,而代码2则只将a的一个字节改为了0。因此,对于不同类型的指针,它解引用后的权限也不一样。
结论1:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作机个字节)。
2.指针+-整数
指针+-整数可以跳过几个字节,但是指针跳过的字节个数不仅与+-的整数有关,还和指针的类型有关。
#include <stdio.h>
int main()
{
int a = 20;
int *pa = &a;
char* pb = (char*)&a;
printf("&a = %p\n", &a);
printf("pa = %p\n", pa);
printf("pa + 1 = %p\n", pa + 1);
printf("pb = %p\n", pb);
printf("pb + 1 = %p\n", pb + 1);
return 0;
}
输出结果:
通过运行结果我们可以看到:char*类型指针+1只跳过了1个字节,int*类型指针+1跳过了4个字节。其实,指针+-1就是跳过一个指向指针的元素。
结论2:指针+-整数决定了指针向前或向后走一步有多大。
四.void*指针
1.概念
void*类型指针可以理解为无具体类型的指针(又叫作泛型指针),他可以接收任意类型的地址,但是他不能进行指针+-整数和解引用操作。
#include <stdio.h>
int main()
{
int a = 30;
char ch = 'w';
void* pa = &a;
void* pc = &ch;
*pa = 10;
*pc = 'e';
return 0;
}
编译结果为:
通过编译结果我们可以看见void*类型指针可以接收不同类型的地址,但是不能进行指针运算。
2.作用
void*类型指针主要用在函数参数部分,用来接收不同类型的指针,这样可以实现泛型编程的效果。具体实现在qsort函数中去讲。
五.const修饰指针
1.const修饰变量
变量的值可以修改的,当const修饰变量之后,变量的值就不能够再修改了,这时该变量称为常变量,但是它的本质还是变量,如果强行改变则编译器会报错。
#include <stdio.h>
int main()
{
int const a = 14;
int n = 0;
a = 20;
n = 30;
printf("%d %d\n", a, n);
return 0;
}
输出结果为:
编译器报错,a的值不能被修改。
但是我们可以绕过a来简间接修改a的值(用指针来修改):
#include <stdio.h>
int main()
{
int const n = 15;
printf("%d\n", n);
int* p = &n;
*p = 38;
printf("%d\n", n);
return 0;
}
输出结果为:
2.const修饰指针变量
const既可以放在*左边,又可以放在*右边,两者的效果有所不同。
(1)const放在*右边
#include <stdio.h>
int main()
{
int n = 15;
int m = 21;
printf("%d\n", n);
int* const pn = &n;
*pn = 32;
printf("%d\n", n);
pn = &m;
return 0;
}
编译结果为:
我们发现此时编译器报错,错误在第13行(pn = &m),因此我们可以知道:当const在*右边时,指针的内容不能被修改,即指针中存放的地址不能被修改,但是指针指向的变量的值可以被修改。
(2)const放在*左边
#include <stdio.h>
int main()
{
int n = 15;
int m = 21;
const int* pn = &n;
printf("%d\n", *pn);
pn = &m;
printf("%d\n", *pn);
*pn = 32;
return 0;
}
编译结果:
此时,结果显示第13行(*pn = 32)有误。即,此时pn指向的变量的值不能够修改,但是pn中存放的地址可以修改。
#include <stdio.h>
int main()
{
int n = 15;
int m = 21;
const int* pn = &n;
printf("%d\n", *pn);
pn = &m;
printf("%d\n", *pn);
return 0;
}
输出结果:
结论:
- 当const在*右边时,修饰的变量是指针本身,保证了指针变量的内容不能被修改,但是指针指向的内容,可以通过指针改变。
- 当const在*左边时,修饰的变量是指针指向的内容,保证指针指向的内容不能通过指针来改变,到那时指针变量本身的内容可变。
六.指针运算
1.指针+-整数
在数组中通过指针+-整数能够找到数组中其他的元素,从而达到遍历数组中每个元素的作用。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
输出结果为:
2.指针- 指针
指针-指针得到的是两个指针之间相隔的元素个数,不是它们指向的元素之间的差值。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = &arr[0];
int* pa1 = &arr[9];
printf("%lld\n", pa1 - pa);
return 0;
}
输出结果:
3.指针的关系运算
指针的关系运算之主要是指针之间进行大小的比较
#include <stdio.h>
int main()
{
int str[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(str) / sizeof(str[0]);
int* p = str;
int i = 0;
while (p < str + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
输出结果为:
七、野指针
1.野指针的成因
(1)指针未初始化就使用
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int* p;//指针未初始化默认为随机值
*p = 10;
printf("%d\n", *p);
return 0;
}
编译结果为:
我们可以看见:此时编译器报错。(使用了未初始化的局部变量p)
(2)指针越界访问
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i < sz + 3; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
输出结果为:
在该代码中,指针p对数组进行了越界访问,我们可以看见此时程序输出了几个小于0的数字。因此,我们要注意指针越界的问题,不然有可能造成程序崩溃。
(3)指针指向的空间被释放
#include <stdio.h>
int* test()
{
int n = 10;
return &n;
}
int main()
{
int a = 25;
int* p = test();
printf("a = %d\n", a);
printf("*p = %d\n", *p);
return 0;
}
输出结果为:
通过观察输出结果我们不难发现*p的值并不是返回的n的值10,这是因为当出了test函数之后,n的空间就被释放了,此时n的地址中放的是一个随机值。
2.如何避免野指针
(1)在使用指针时要初始化指针,当不知道初始化为什么值的时候我们可以将他置为NULL。
当指针被置为NULL时,若强行使用则编译器会报错,代码如下:
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
int* ps = NULL;
int str[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(ps + i));//未对指针进行初始化就使用
}
return 0;
}
编译结果为:
这时,如果我们想使用ps指针,则要对其进行初始化。
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
int* ps = NULL;
int str[10] = { 1,2,3,4,5,6,7,8,9,10 };
ps = str;//对指针ps进行初始化
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(ps + i));
}
return 0;
}
输出结果为:
这时我们可以看见编译器输出了正确的结果。因此,我们要对指针进行初始化或者直接置为NULL。
(2)防止指针越界
用指针访问数组时要注意数组的范围,防止指针越界而造成错误。
(3)避免返回局部变量的地址
每个局部变量都有自己的作用域,当离开局部变量的作用域时,局部变量的空间就会被释放,此时,局部变量的空间中存放的是随机值。
(4)当指针使用完之后要及时置为NULL,使用之前要检查它的有效性。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,4,3,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
//检查pa的有效性
if (*pa != NULL)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(pa + i));
}
}
pa = NULL;//将pa置为NULL
return 0;
}
我们也可以用assert宏来检验指针的有效性
assert()接收一个表达式作为参数,如果表达式为真,则程序继续运行assert不会产生任何作用;若果表达式为假,assert()就会报错,在标准错误流stderr中写入错误信息,以及包含这个表达式的文件名和行号。assert包含在头文件<assert.h>中。
八、传值调用和传址调用
1.传值调用
在调用函数时,将参数本身传递给函数的这种调用方式我们叫做函数的传值调用。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 25;
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
输出结果:
结论:
实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参。
2.指针的传址调用
在调用函数时,将参数的地址传递给函数的这种调用方式我们叫做函数的传址调用。
#include <stdio.h>
void num_transform(int* pa, int* pb)
{
int c = *pa;
*pa = *pb;
*pb = c;
}
int main()
{
int a = 10;
int b = 35;
printf("交换前a = %d,b = %d\n", a, b);
num_transform(&a, &b);
printf("交换后a = %d,b = %d\n", a, b);
return 0;
}
输出结果为:
在这里我们将a和b的地址传递给了函数num_transform ,然后通过a和b的地址改变了a和b中的值。
结论:
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数的变量。
九、指针和数组
1.数组名
(1)数组名是首元素的地址
#include <stdio.h>
int main()
{
int str[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&str[0] = %p\n", &str[0]);
printf("str = %p\n", str);
return 0;
}
输出结果为:
我们可以看见str和str[0]的地址相同,数组名就是首元素的地址。但是,有两个例外:
(1)sizeof(数组名)
sizeof中放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
#include <stdio.h>
int main()
{
int str[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(str);
printf("%d\n", sz);
return 0;
}
输出结果:
由于每个整型的大小是4个字节,str数组中一共有10个整数,因此str的大小为40个字节。
(2)&数组名
这里的数组名也是表示的是整个数组的地址,取出的是整个数组的地址。
#include <stdio.h>
int main()
{
int str[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&str = %p\n", &str);
return 0;
}
调试观察:
输出结果为:
通过调试我们发现:打印出的是首元素的地址 。在学习数组的时候我们知道数组在内存中是连续存放的。因此,我们只要知道了数组首元素的地址就能找到数组中所有元素的地址。
&arr是整个数组的地址,因此&arr + 1会跳过sizeof(arr)个字节(一个数组的大小);而&arr[0] + 1只会跳过sizeof(arr[0])个字节(一个元素的大小)。
2.一维数组传参的本质
一维数组传参传的是首元素的地址
#include <stdio.h>
void test(int arr[])
{
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("sz = %d\n", sz);
test(arr);
return 0;
}
输出结果为:
由于数组名是首元素的地址,数组传参的时候传的数组名,因此本质上数组传参传递的是数组首元素的地址。
结论:
一维数组传参我们既可以用数组接收,也可以用指针来接收。
十、二级指针
1.概念
二级指针就是存放指针变量的地址的指针。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int** paa = &pa;
printf("&pa = %p\n", &pa);
printf("paa = %p\n", paa);
return 0;
}
输出结果:
我们可以发现&pa和paa的地址是相同的,因为 pa为二级指针,它存放的是pa的地址。
2.二级指针的运算
(1) 找到一级指针的地址,对一级指针进行访问
#include <stdio.h>
int main()
{
int a = 20;
int* pa = &a;
int** paa = &pa;
printf("*paa = %p\n", *paa);
return 0;
}
(2)找到一级指针中存储的内容
#include <stdio.h>
int main()
{
int a = 20;
int* pa = &a;
int** paa = &pa;
**paa = 35;
printf("a = %d\n", a);
return 0;
}
输出结果为:
十一、指针数组
1.概念
存放指针的数组叫做指针数组 。
2.声明与定义
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* str[3] = { arr1,arr2,arr2 };//指针数组的声明和定义
3.指针数组模拟实现二维数组
#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 sz = sizeof(arr1) / sizeof(arr1[0]);
int* str[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < sz; j++)
{
printf("%d ", *(*(str + i) + j));
}
printf("\n");
}
return 0;
}
输出结果:
*(str+i)访问的是str中的每个元素,str中的元素都是一维数组首元素的地址, 因此*(*(str+1)+j)访问的是每个一维数组中的元素。
数组指针模拟的二维数组并不是真正的二维数组,应为该”二维数组“的每一行并不是连续的。