一:指针的理解
(1)变量的访问方式
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 ,为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元通常占用1个字节。
变量在内存中分配空间,不同类型的变量占用不同大小的空间,那如何访问内存中的变量数据呢?有两种方式:
- 直接访问,直接使用变量名进行的访问,以前的程序中都是采用这种方式。
- 间接访问,通过指针来实现。
(2)内存地址
为了能够有效地访问到每个内存单元,就给内存单元进行了编号,这些编号被称为内存地址,因为每个内存单元都有地址,所以变量也是有地址的。
假设有 int 型变量 num,其在内存中会占用4个字节,也就是占用4个内存单元,第一个内存单元的地址即是变量 num 的地址。
在32位系统中,内存地址通常是32位二进制数字,即4个字节,这允许寻址2^32(大约 4GB)个内存位置。
在64位系统中,内存地址通常是64位二进制数字,即8个字节,这允许寻址2^64个内存位置,这个数量相当大,远远超过了通常需要的内存大小。
(3)什么是指针
如果一个变量专门用来存放内存地址,则它称为指针变量,通常简称为指针。我们可以通过指针间接访问内存中另一个数据。
指针里面存储的是变量 num 的地址,我们可以说该指针指向变量 num,通过该指针可以间接访问变量 num。
(4)指针的定义
数据类型 *指针变量名 = 初始地址值;
数据类型是指针所指向的地址处的数据类型,如 int、char、float 等。
符号 * 用于通知系统,这里定义的是一个指针变量,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char * 表示一个指向字符的指针,float * 表示一个指向浮点数的指针。
需要注意的是,以下三种定义指针的写法都是合法的:
int *ptr;
int* ptr;
int * ptr;
(5)取址运算符和取值运算符
取址运算符,使用 & 符号表示,作用是取出变量的内存地址。如果要格式化输出地址,需使用格式占位符 %p。
取值运算符,使用 * 符号表示,作用是取出指针指向的内存地址处的数据值,也称为解引用运算符或间接引用运算符。
#include <stdio.h>
int main()
{
// 定义一个整型变量
int a = 100;
double b = 200;
// 取地址->取内存当中编号 &
// 地址的格式占位符%p
printf("a的地址:%p\n", &a);
printf("b的地址:%p\n", &b);
// 指针即为变量,只不过用来存储地址而已
int *ptr = &a;
double *ptr1 = &b;
printf("ptr指针变量存储地址:%p\n", ptr);
printf("ptr1指针变量存储地址:%p\n", ptr1);
// 访问数据
printf("变量a存储的数据是%d\n", a);
printf("通过指针ptr访问:%d\n", *ptr);
// 访问数据
printf("变量b存储的数据是%lf\n", b);
printf("通过指针变量访问数据:%lf\n", *ptr1);
// 通过指针修改内存当中数据
*ptr = 200;
*ptr1 = 6.9;
printf("通过指针ptr访问:%d\n", *ptr);
printf("通过指针变量访问数据:%lf\n", *ptr1);
printf("变量a:%d\n",a);
printf("变量b:%lf\n",b);
return 0;
}
#include <stdio.h>
int main()
{
// 声明一个变量并且同时赋值
int num = 110;
// 创建指针变量的语法:指针就是变量,只不过这个变量存储的是地址
int *ptr = #
// 打印变量的数值
printf("num=%d\n", num);
// 打印变量num的内存的地址 取址&
printf("num变量的开辟空间地址:%p\n", &num);
// 打印指针的值
printf("指针变量ptr存储数值:%p\n", ptr);
// 打印指针的地址
printf("输出指针ptr的地址:%p\n", &ptr);
// 打印指针指向的数值 *取值
printf("输出数值:%d\n", *ptr);
// 查看一下这两个变量在内存当中占用字节大小
printf("num占房间个数:%zu\n", sizeof(num));
printf("指针变量ptr占用几个房间个数:%zu\n", sizeof(ptr));
return 0;
}
二:指针运算
(1)指针加减整数
指针与整数的加减运算,表示指针所指向的内存地址的移动(加,向后移动;减,向前移动),指针移动多少,与指针指向的数据类型有关,数据类型占据多少个字节,每单位就移动多少个字节,比如一个 int 类型指针,+1 向后移动 4 个字节,-2 向前移动 8 个字节。
数组的元素在内存中连续存储的,我们通过数组元素来演示指针加减整数的情况。
#include <stdio.h>
int main()
{
// 声明一个数组
int arr[5] = {10, 20, 30, 40, 50};
// 每一个元素的地址
printf("第1个元素的地址:%p\n", &arr[0]);
printf("第2个元素的地址:%p\n", &arr[1]);
printf("第3个元素的地址:%p\n", &arr[2]);
printf("第4个元素的地址:%p\n", &arr[3]);
printf("第5个元素的地址:%p\n", &arr[4]);
// 定义指向数组第一个元素10指针
int *ptr = &arr[0];
printf("ptr变量存储的数据:%p\n", ptr);
ptr += 1;
printf("ptr+=1=%p\n", ptr);
ptr += 3;
printf("ptr+=3=%p\n", ptr);
ptr -= 2;
printf("ptr+=3=%p\n", ptr);
//取值
printf("%d\n",*ptr);
return 0;
}
(2)指针自增自减
指针自增、自减本质上就是指针加减整数,自增地址后移,自减地址前移。下面我们利用指针的自增自减实现数组的遍历。
#include <stdio.h>
int main()
{
// 定义数组
int arr[5] = {10, 20, 30, 40, 50};
// 遍历数组
for (int i = 0; i < 5; i++)
{
printf("数组的元素:%d\n", arr[i]);
}
/***通过指针自增自减实现遍历数组*****/
int *ptr = &arr[0];
// 通过指针遍历数组
for (int i = 0; i < 5; i++)
{
printf("%d\n", *ptr);
ptr++;
}
// 指针遍历倒着这来
for (int i = 0; i < 5; i++)
{
ptr--;
printf("+++++%d\n", *ptr);
}
return 0;
}
#include <stdio.h>
int main()
{
// 指针遍历数组-对于程序员而言,没有太大意义
int arr[5] = {10, 20, 30, 40, 50};
// 遍历数组
for (int i = 0; i < 5; i++)
{
// 实质角标这种写法,底层就是指针的运算符
printf("%d\n", arr[i]);
}
int *ptr = &arr[0];
for (int i = 0; i < 5; i++)
{
printf("%d\n", *(i + ptr));
}
return 0;
}
(3)同类型指针相减
相同类型的指针可以进行减法运算,返回它们之间的距离,即相隔多少个数据单位。高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。
同类型指针相减的结果是一个 ptrdiff_t 类型数据,ptrdiff_t 类型是一个带符号的整数,格式输出中对应的格式占位符是 %td。
#include <stdio.h>
int main()
{
// 相同类型的指针---减法运算
int arr[5] = {10, 20, 30, 40, 50};
// // 数组第一个元素
int *ptr1 = &arr[0];
// // 数组第四个元素
// int *ptr4 = &arr[3];
// printf("ptr4 - ptr1 = %td\n", ptr4 - ptr1);
// printf("ptr1 - ptr4 = %td\n", ptr1 - ptr4);
// 相同类型指针-进行减法运算符
// ptr1 + ptr4;
double a = 3.14;
double b = 6.88;
double *ptr2 = &a;
double *ptr3 = &b;
printf("%td\n", ptr3 - ptr2);
//不同类型的指针不能减法运算
// ptr3 - ptr1;
return 0;
}
(4)指针的比较运算
指针之间可以进行比较运算,如 ==、<、 <= 、 >、 >=,比较的是各自指向的内存地址的大小,返回值是 int 类型整数 1 (true)或 0 (false)。
#include <stdio.h>
int main()
{
int arr[5] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[0];
int *ptr4 = &arr[4];
// 指针比较大小
printf("ptr1==ptr4:%d\n", ptr1 == ptr4);
printf("ptr1>ptr4:%d\n", ptr1 > ptr4);
printf("ptr1<ptr4:%d\n", ptr1 < ptr4);
double num = 1.1;
double *ptr5 = #
// 下面会产生警告
// printf("ptr5>ptr4:%d\n", ptr5 > ptr4);
return 0;
}
三:指针和数组
(1)数组名
数组名在大多数情况下会被隐式地转换为指向数组第一个元素的指针,在特定情况下数组名可以被视为一个指针,具有一些指针的特性。
但是数组名与真正的指针是不同的,主要有以下几点区别:
- 使用 sizeof 运算符,数组名得到的是整个数组的大小;指针得到的是本身的大小。
- 数组名不能进行自增、自减运算。
- 数组名的指向不可更改。
#include <stdio.h>
int main()
{
// 数组
int arr[5] = {10, 20, 30, 40, 50};
printf("第1个元素地址:%p\n", &arr[0]);
printf("第2个元素地址:%p\n", &arr[1]);
printf("第3个元素地址:%p\n", &arr[2]);
printf("第4个元素地址:%p\n", &arr[3]);
printf("第5个元素地址:%p\n", &arr[4]);
/**************************************/
int *ptr = &arr[0];
printf("arr数组名:%p\n", arr);
printf("ptr指针变量保存的地址:%p\n", ptr);
// 验证占用内存大小
printf("arr占用内存大小:%zu\n", sizeof(arr));
printf("指针占用大小:%zu\n", sizeof(ptr));
// 指针自增++
ptr++;
printf("ptr指向的地址:%p,保存的数值:%d\n", ptr, *ptr);
// 数组名
// arr++;
int num = 1688;
ptr = #
// arr = #
printf("ptr保存的地址:%p,存储的数据:%d\n", ptr, *ptr);
return 0;
}
(2)指针数组
指针数组(Pointer Array)是一个数组,其中的每个元素都是指针。
语法规则:
数据类型 *指针数组名[长度];
#include <stdio.h>
// 指针数组:就是数组,只不过数组的元素是指针!
int main()
{
// 数组
// int arr[5] = {10, 20, 30, 40, 50};
// 指针数组
int a = 10, b = 20, c = 30, d = 40, e = 50;
// 五个指针
int *ptr = &a;
int *ptr1 = &b;
int *ptr2 = &c;
int *ptr3 = &d;
int *ptr4 = &e;
// 声明一个指针数组
int *arr[5];
arr[0] = ptr;
arr[1] = ptr1;
arr[2] = ptr2;
arr[3] = ptr3;
arr[4] = ptr4;
// 遍历指针数组
for (int i = 0; i < 5; i++)
{
printf("指针数组内部元素:%p,查看一下每个元素地址上数据%d\n", arr[i], *arr[i]);
}
return 0;
}
(3)数组指针
数组指针(Array Pointer)是一个指针,它指向一个数组。注意,数组指针指向的是整个数组的地址而不是第一个元素的地址,虽然二者值是相同的,但在运算中会表现出不同。
语法规则:
数据类型 (*数组指针名)[长度];
#include <stdio.h>
int main()
{
// 定义一个数组
int arr[5] = {10, 20, 30, 40, 50};
// 数组指针
int(*ptr)[5] = &arr;
// 数组的第一个元素的地址
int *ptr1 = &arr[0];
// printf("prt=%p\n", ptr);
// printf("ptr1=%p\n", ptr1);
printf("第1个元素的地址:%p\n", &arr[0]);
printf("第2个元素的地址:%p\n", &arr[1]);
printf("第3个元素的地址:%p\n", &arr[2]);
printf("第4个元素的地址:%p\n", &arr[3]);
printf("第5个元素的地址:%p\n", &arr[4]);
printf("ptr1+=1:%p\n", ptr1 + 1);
printf("ptr+=1:%p\n", ptr + 1);
// 遍历数组
for (int i = 0; i < 5; i++)
{
printf("数组中的%d索引的元素值为:%d\n", i, (*ptr)[i]);
}
return 0;
}
(4)数组指针和数组名的区别
- 指向不同:数组名指向元素首地址,数组指针指向数组的地址。
- 类型不同:上面案例中,arr 的类型是 int[5],;ptr 的类型是 int(*)[5]。
- 可变性:数组名通常是不可变的;数组指针是可变的,你可以将它指向不同的数组。
- 初始化:数组名不需要显式初始化,它会自动指向数组的首元素;数组指针需要显式初始化,指定它将指向的数组。
- 访问元素:数组名访问数组元素不需要解引用;数组指针通常需要解引用才能访问数组元素。
(5)字符指针
字符指针变量(简称字符指针)是C语言中的一种指针类型,它用于指向字符或字符串(字符数组),通常用于处理字符串(字符数组)。
char *pStr= "hello tom";
C语言对字符串" hello tom"是按字符数组处理的,在内存中开辟了一个字符数组用来存放字符串,程序在定义字符串指针pStr时只是把字符串首地址(即存放字符串的字符数组的首地址)赋给pStr。
#include <stdio.h>
int main()
{
// 字符数组->字符串
// char str[3];
// // str = "ahahahah"; 数组的名字是常量不能像这样从新赋值
// str[0] = 'a';
// str[1] = 'b';
// 字符指针
char *str = "one two three!";
str = "good good study day day up!";
printf("%c", *str);
printf("%s", str);
return 0;
}
对字符数组只能对各个元素赋值,不能对字符数组名重新赋值。
char str[14];
str=" hello tom"; //错误
str[0] = 'i'; //ok
字符指针是可变的,允许使用下面方法重新赋值,指向新的字符串。
char *a = "hello tom";
a = "tom";
四:指针和函数
(1)传递指针给函数
当函数的形参类型是指针类型时,使用该函数时,需要传递指针、地址或者数组给该形参。
传地址或指针给函数
#include <stdio.h>
// 函数原型
void fn(int *);
int main()
{
int a = 100;
int *ptr = &a;
printf("a的地址:%p,a地盘存储的数据:%d\n", &a, a);
// 调用函数
fn(ptr);
printf("!!!函数调用结束a的地址:%p,a地盘存储的数据:%d\n", &a, a);
fn(&a);
printf("!!!函数调用结束a的地址:%p,a地盘存储的数据:%d\n", &a, a);
return 0;
}
// 声明函数
void fn(int *p)
{
printf("aaaaaaaa---p=%p,*p=%d\n", p, *p);
*p += 1;
printf("aaaaaaaa---p=%p,*p=%d\n", p, *p);
}
传数组给函数
数组名本身就代表数组首地址,因此传数组的本质就是传地址。
#include <stdio.h>
// 函数:返回这个数组的平均数
double fn(int *array, int length)
{
double sum = 0;
for (int i = 0; i < length; i++)
{
// printf("%d\n", array[i]);
// printf("%d\n",*(array + i))
sum += array[i];
// 第一种写法
// sum += *array;
// array++; // 从数组首个元素地址向后移动
}
return sum / length;
}
int main()
{
int arr[5] = {11, 3, 89, 1278, 45678};
// 计算出数组的长度
int length = sizeof(arr) / sizeof(int);
double result = fn(arr, length);
printf("平均数:%lf\n", result);
return 0;
}
(2)指针函数(返回指针的函数)
如果函数的返回值是一个指针,这样的函数称为指针函数。
语法规则:
返回类型 *指针函数名(参数列表)
#include <stdio.h>
#include <string.h>
char *strlong(char *str1, char *str2)
{
// 三元可以用if...else代替
return strlen(str1) >= strlen(str2) ? str1 : str2;
}
int main()
{
// 想让用户输入两个字符串
char str1[100], str2[100], *str3;
// 提示
printf("请您输入一段话:");
scanf("%s", &str1);
printf("请您在输入一段话:");
scanf("%s", &str2);
str3 = strlong(str1, str2);
printf("%s比较长\n", str3);
return 0;
}
返回值不能指向局部变量
函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,所以函数返回的指针不能指向这些数据。
如果确实有这样的需求,需要定义为静态局部变量。
#include <stdio.h>
int *fn()
{
// 局部变量:函数执行完毕立马销毁
// 因为局部变量函数调用完毕,内存被收回!!!
static int num = 100;
// 返回变量的地址
return #
}
int main()
{
// 调用函数
int *ptr = fn();
printf("ptr地址:%p,存储的数据:%d\n", ptr, *ptr);
return 0;
}
应用案例
编写一个函数,它会生成10个随机数,并使用数组名作为返回值。
说明:生成随机数可以使用 rand() 函数,由C语言标准库 <stdlib.h> 提供,可以返回一个随机整数。
#include <stdio.h>
#include <stdlib.h>
int *fn()
{
// 数组
static int arr[10];
// 给数组添加元素
for (int i = 0; i < 10; i++)
{
arr[i] = rand();
}
return arr;
}
int main()
{
// 调用函数
int *array = fn();
// 使用数组
for (int i = 0; i < 10; i++)
{
printf("第%d元素的数值是%d\n", i, array[i]);
}
return 0;
}
(3)函数指针(指向函数的指针)
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数,这种指针就是函数指针。
语法规则:
返回类型 (*函数指针名)(参数列表);
注意:()的优先级高于*,第一个括号不能省略,如果省略,就成了指针函数。
用函数指针来实现对函数的调用,返回两个整数中的最大值。
#include <stdio.h>
// 声明一个函数:两个形参比较大小,返回较大的数值
int max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
// 函数指针:就是指针,只不过保存的是函数的地址而已!!!
int (*ptr)(int a, int b) = max;
// 获取最大数值
int result = (*ptr)(10, 20);
printf("result===%d\n", result);
return 0;
}
(4)回调函数
函数指针可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。
简单的讲,回调函数是由别的函数执行时调用你传入的函数。
不使用回调:
#include <stdio.h>
#include <stdlib.h>
// 封装一个函数:函数最终返回一个数组,有十个随机数字!!!
int *initArray()
{
// 声明一个数组
static int arr[10];
for (int i = 0; i < 10; i++)
{
arr[i] = rand();
}
return arr;
}
int main()
{
// 初始化一个整型的数组(10个随机的元素)
int *array = initArray();
// 循环遍历数组
for (int i = 0; i < 10; i++)
{
printf("%d\n", array[i]);
}
return 0;
}
使用回调函数:
#include <stdio.h>
#include <stdlib.h>
// 回调函数:某一个函数的形参存储的数据是一个函数,形参称之为回调函数!!!
int *initArray(int *array, int length, int (*ptr)())
{
for (int i = 0; i < length; i++)
{
array[i] = (*ptr)();
}
return array;
}
int main()
{
// 声明一个数组
int arr[10];
// 数组长度
int length = sizeof(arr) / sizeof(int);
initArray(arr, length, rand);
// 打印数组内部元素
for (int i = 0; i < 10; i++)
{
printf("数组的元素:%d,数值为%d\n", i, arr[i]);
}
return 0;
}
五:指向指针的指针(多级指针)
指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
声明多级指针时,需要使用多个星号来表示指针的级别。
#include <stdio.h>
int main()
{
int a = 100;
int *ptr1 = &a; // 一级指针
int **ptr2 = &ptr1; // 二级指针
int ***ptr3 = &ptr2; // 三级指针
printf("ptr1=%d\n", *ptr1);
printf("ptr2=%d\n", **ptr2);
printf("ptr3=%d\n", ***ptr3);
return 0;
}
六:空指针
赋为NULL 值的指针被称为空指针,NULL 是一个定义在标准库 <stdio.h>中的值为零的宏常量。
声明指针变量的时候,如果没有确切的地址赋值,为指针变量赋一个NULL 值是好的编程习惯。
#include <stdio.h>
int main()
{
// 定义了一个指针,但是你不知道指针指向那块地址
int *ptr = NULL;
int a = 100;
ptr = &a;
printf("%d\n", *ptr);
// 定义指针变量
int *ptr1;
return 0;
}
七:野指针
野指针就是指针指向的位置是不可知(随机性,不正确,没有明确限制的)。
(1)野指针的成因
指针使用前未初始化
指针变量在定义时如果未初始化,其值是随机的,此时操作指针就是去访问一个不确定的地址,所以结果是不可知的。此时p就为野指针。
在没有给指针变量显式初始化的情况下,一系列的操作(包括修改指向内存的数据的值)也是错误的。
int *p;
printf("%d\n", *p);
指针越界访问
int arr[4] = {10,20,30,40};
int *p = arr;
p += 4;
printf("%d", *p); // 此时 *p 即为越界访问
当 p += 4之后,此时 *p 访问的内存空间不在数组有效范围内,此时 *p 就属于非法访问内存空间,p为野指针。
指针指向已释放的空间
#include <stdio.h>
int *test()
{
int a = 10;
return &a; //&a=0x0012ff40
}
int main()
{
int *p = test();
printf("%d", *p);
return 0;
}
调用test()函数将返回值赋给p,test函数的返回值是局部变量a的地址,函数调用结束局部变量会被销毁,其所占用的内存空间会被释放,p 指向的是已经释放的内存空间,所以 p 是野指针。
(2)如何避免野指针
- 指针初始化如果没有确切的地址赋值,为指针变量赋一个 NULL 值是好的编程习惯。
- 小心指针越界。
- 避免返回指向局部变量的指针。
- 指针使用之前检查指针是否为 NULL。
附录
变量定义 | 类型表示 | 含义 |
int i | int | i是一个整型变量。 |
int *p | int * | p 是一个指向整型数据的指针 |
int a[5] | int[5] | a 是一个5个元素的整型数组。 |
int *p[4] | int *[4] | p 是一个指针数组,每个元素都是指向整型数据的指针。 |
int (*p)[4] | Int (*)[4] | p 是一个数组指针,指向一个4个元素的整型数组。 |
int f() | Int () | f是一个返回整型数据的函数 |
int *f() | int *() | f是一个指针函数,返回指向整数数据的指针。 |
int (*p)() | int (*)() | p 是一个函数指针,指向一个返回整型数据的函数。 |
int **p | int ** | p是一个指向指针的指针。 |
八:本章练习
(1)习题一
请写出下面程序的运行结果
int num = 250;
int *p1 = #
int **p2 = &p1;
int ***p3 = &p2;
printf("%zu %zu %zu %zu", sizeof p3, sizeof *p3, sizeof **p3, sizeof ***p3);
答案:8 8 8 4
解析:
- p3 的值是 p2 的地址。
- *p3 得到的是 p1 的地址。
- **p3 得到的是 num 的地址。
- *** p3 得到的是 num 的值 250。
(2)习题二
请分别写出声明指针函数和函数指针的代码
// 声明指针函数
int *fn1();
// 声明函数指针
int (*fn2)();
(3)习题三
请写出至少两种野指针的成因
答案:
- 指针使用前未初始化
- 指针越界访问
- 指针指针已经释放的空间
(4)习题四
请写出下面程序的运行结果
char msg[] = "Hello World";
char *ptr = msg;
ptr = "Hello Tom";
msg[1] = 'a';
printf("%s", ptr);
答案:Hello Tom
解析:
- 创建字符数组msg。
- 字符指针ptr指向字符数组msg的首元素。
- 字符指针ptr指向了一个新的字符串常量。
- 修改字符数组msg的第二个元素,但此时字符指针 ptr 和 字符数组 msg 已经没有关系了。
- 输出字符指针所指向的值,结果是 Hello Tom。
本章内容到此结束,下一章为自定义数据类型
关注我,一起学习嵌入式
标签:11,arr,int,嵌入式,数组,printf,C语言,ptr,指针 From: https://blog.csdn.net/mkk521/article/details/140935091