当前位置: 首页 > news >正文

C语言进阶之路-数组与指针

目录

一、学习目标

二、数组入门

基本概念

语法释义:

定义:

访问:

赋值:

字符数组

多维数组

数组万能拆解法

三、指针入门

内存地址

基地址

取址符

指针基础

指针的定义:

指针的赋值

指针的尺寸

四、数组进阶

数组名涵义

数组下标

字符串('\0')常量

零长数组

变长数组

五、指针进阶

char型指针

多级指针

指针万能拆解法

void型指针

野指针

空指针

指针的加减:

const 型指针

函数指针

总结


一、学习目标

  • 数组的基础入门
  • 指针的基础入门
  • 数组和指针的阶级

二、数组入门

基本概念

  • 逻辑:一次性定义多个相同类型的变量,并存储到一片连续的内存中
  • 示例:
数据类型  变量名 [ 数量 ] ;
int a[5];

  • 语法释义

    • a 是数组名,即这片连续内存的名称
    • [5] 代表这片连续内存总共分成5个相等的格子,每个格子称为数组的元素
    • int 代表每个元素的类型,可以是任意基本类型,也可以是组合类型,甚至可以是数组

定义:

int a [5] ;

访问:

         初始化:

只有在定义的过程中顺便赋值,称为初始化。只有在初始化的时候可以对这个数组进行连续赋值。

int  a [5] = {1,2,3,4,5} ;  // 初始化
int a [5] ;  // 可行,但是该数组中的数据内容是不确定的
int a[5] = {100,200,300,400,500,600}; // 警告,越界了,编译器会把越界部分直接放弃 600 
int a[ ] = {100,200,300}; // OK,自动根据初始化列表分配数组元素个数
int a [] ; // 【错误】数组在定义申请内存的过程中 【无法确定内存大小】 
int a[5] = {100,200,300}; // OK,只初始化数组元素的一部分,未初始化部分被默认设置为0

赋值:

a[0] = 6; a[1] = 7;

数组元素的引用

  • 存储模式:一片连续的内存,按数据类型分割成若干相同大小的格子
  • 元素下标:数组开头位置的偏移量

元素下标偏移量

  • 示例:
int a[5]; // 有效的下标范围是 0 ~ 4
a[0] = 1;
a[1] = 66;
a[2] = 21;
a[3] = 4;
a[4] = 934;a[5] = 62; // 错误,越界了
a    = 10; // 错误,不可对【数组名】赋值 

字符数组

  • 概念:专门存放字符的数组,称为字符数组
  • 初始化与元素引用:
char s1[5] = {'a', 'b', 'c', 'd', 'e'};       // s1存放的是【字符序列】,非字符串
char s2[6] = {'a', 'b', 'c', 'd', 'e', '\0'}; // s2存放了一个【字符串】char s[6] = {"abcde"}; // 使用字符串直接初始化字符数组【字符串】
char s[6] =  "abcde" ; // 大括号可以省略【字符串】
char s[5] =  "abcde" ; // 【字符序列】s[0] = 'A'; // 索引第一个元素,赋值为 'A'

多维数组

  • 概念:若数组元素类型也是数组,则该数组称为多维数组
  • 示例:
int a[2][3];// 代码释义:
// 1, a[2]   是数组的定义,表示该数组拥有两个元素
// 2, int [3]是元素的类型,表示该数组元素是一个具有三个元素的整型数组

二维数组的初始化:

int  a[2][3] ; // 定义了二维数组但是没有进行初始化,因此数组中的数据是随机的。
int  a[2][3] = {  {1,2,3}  ,  {9,8,7} };  // 初始化整个数组
int  a[2][3] = {  {1,2}  ,  {9,8} };  // 初始化二维数组中的小素组的的时候可以不完全初始化
int  a[2][3] = { 1,2,3 ,9 , 8 , 7} ; // 初始化整个数组 (按顺序进行初始化)int a[2][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 错误,越界了, 多出来的 {7,8,9} 会被编译器丢弃int a[2][3] = {{1,2,3}, {4,5,6,7}};        // 错误,越界了 , 多出来的 7 会被编译器丢弃int a[ ][3] = {{1,2,3}, {4,5,6}}; // OK,自动根据初始化列表分配数组元素个数 a[2]
int a[2][ ] = {{1,2,3}, {4,5,6}}; // [错误]  int  []  类型不完成无法正确分配内存空间
int a[2][3] = {{1,2,3}};          // OK,只初始化数组元素的一部分

打怪实战

  • 定义一个用于存放多个学生姓名的数组。
    • 访问数据
      • 键盘输入新的学生姓名
      • 遍历打印所有的学生姓名
    • 尝试对于他进行初始化
    • 基础练习:使用整形数组完成以上练习

数组万能拆解法

  • 任意的数组,不管有多复杂,其定义都由两部分组成。
    • 第1部分:说明元素的类型,可以是任意的类型(除了函数)
    • 第1部分:说明数组名和元素个数

  • 示例:
int   a[4];       // 第2部分:a[4]; 第1部分:int
int   b[3][4];    // 第2部分:b[3]; 第1部分:int [4]
int   c[2][3][4]; // 第2部分:c[2]; 第1部分:int [3][4]
int  *d[6];       // 第2部分:d[6]; 第1部分:int *
int (*e[7])(int, float); // 第2部分:e[7]; 第1部分:int (*)(int, float)

注解:

    • 上述示例中,a[4]、b[3]、c[2]、d[6]、e[7]本质上并无区别,它们均是数组
    • 上述示例中,a[4]、b[3]、c[2]、d[6]、e[7]唯一的不同,是它们所存放的元素类型的不同
    • 第1部分的声明语句,如果由多个单词组成,C语言规定需要将其拆散写到第2部分的两边

三、指针入门

内存地址

  • 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
  • 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。

基地址

  • 单字节数据:对于单字节数据而言,其地址就是其字节编号。
  • 多字节数据:对于多字节数据而言,期地址是其所有字节中编号最小的那个,称为基地址(入口地址)。

取址符

  • 每个变量都是一块内存,都可以通过取址符 & 获取其地址
  • 例如:
int a = 100;
printf("整型变量 a 的地址是: %p\n", &a);char c = 'x';
printf("字符变量 c 的地址是: %p\n", &c);double f = 3.14;
printf("浮点变量 f 的地址是: %p\n", &f);

注意:

  • 内存地址的大小取决于系统的位数。
    • 虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸却是一样的。
    • 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
char *     p1;  // 尺寸为 8字节, 指向的内存应该是1个字节的
short *    p2;  // 尺寸为 8字节, 指向的内存应该是2个字节的
int *      p3;  // 尺寸为 8字节, 指向的内存应该是4个字节的
long *     p4;  // 尺寸为 8字节, 指向的内存应该是8个字节的

指针基础

  • 指针的概念:
    • 地址。比如 &a 是一个地址,也是一个指针,&a 指向变量 a。
    • 专门用于存储地址的变量,又称指针变量。

理解:

& 取地址符 --> 取得某一个变量的地址

* 解引用符 --> 去地址(去到某一个地址中访问数据)

语法:

指针指向的类型 * 指针变量名 ;

  • 指针的定义:

int    *p1; // 用于存储 int  型数据的地址,p1 被称为 int  型指针,或称整型指针
char   *p2; // 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
double *p3; // 用于存储double型数据的地址,p3 被称为 double 型指针
  • 指针的赋值

    • 把某一个对应类型的地址存入到指针变量中,类型需跟指针的类型相匹配。
    • 指针变量专门用于存放地址
int a = 100;
p1 = &a; // 将一个整型地址,赋值给整型指针p1char c = 'x';
p2 = &c; // 将一个字符地址,赋值给字符指针p2double f = 3.14;
p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
  • 指针的索引:通过指针,取得其指向的目标
*p1 = 200; // 将 p1 指向的目标(即a)修改为200,等价于 a = 200;
*p2 = 'y'; // 将 p2 指向的目标(即c)修改为'y',等价于 c = 'y';
*p3 = 6.6; // 将 p3 指向的目标(即f)修改为6.6,等价于 f = 6.6;

指针的尺寸

  • 指针尺寸指的是指针变量自己所占内存的字节数
  • 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
  • 结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关

小怪实战

  • 尝试实现随机点名
    • 【拓展】参考C语言课程的小项目效果进行拓展
    • 通过随机数产生一个数组下标然后把对应的名字输出
    • 从键盘中获取学生的姓名 (假设数组有10个元素则可以先在代码中写8个通过键盘获取剩余的两个)
    • 使用二维数组存储N个学生姓名
// 使用二维数组实现随机点名#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>int main(int argc, char const *argv[])
{char NameS[100][32] = {"Even",    // NameS[0]"Jacy",     // NameS[1]"TieZhu","ErGou","CuiHua","DaChui",     // NameS[5]"ZhangSan" ,      // NameS[6]"Yilia","WangDaChui","TieDan",};// for (int i = 0; i < 3 ; i++)// {//     printf("请输入一个姓名:\n");//     scanf("%s" , NameS[i+7]);// }// 设置随机种子srand((int)time(0));int index ;// 每间隔0.3秒出现一个名字for (int i = 0; i < 10; i++){// 产生随机数index = (int) (10.0 * rand() / (RAND_MAX + 1.0));// printf("index:%d\r" , index);   // 往标准输出中打印数据,因此如果没有\n则数据不会立即显示// fprintf( stderr , "\rindex:%d" , index  );fprintf( stderr ,"\r幸运观众:%s" , NameS[index]); // fprintf可以指定输出的目标文件 ///  stderr 则是标准出错文件,文件没有缓冲区 , 只要有数据则立即显示// 1s = 1000 ms  = 1000000us usleep(300000);fprintf( stderr ,"\r                          " ); // 使用空格当前这一行数据}// 每间隔0.5秒出现一个名字for (int i = 0; i < 5; i++){// 产生随机数index = (int) (10.0 * rand() / (RAND_MAX + 1.0));// printf("index:%d\r" , index);// fprintf( stderr , "\rindex:%d" , index  );fprintf( stderr ,"\r幸运观众:%s" , NameS[index]);// 1s = 1000 ms  = 1000000us usleep(500000);fprintf( stderr ,"\r                          " );}// 每间隔1秒出现一个名字for (int i = 0; i < 3; i++){// 产生随机数index = (int) (10.0 * rand() / (RAND_MAX + 1.0));// printf("index:%d\r" , index);// fprintf( stderr , "\rindex:%d" , index  );fprintf( stderr ,"\r幸运观众:%s" , NameS[index]);// 1s = 1000 ms  = 1000000us sleep(1);fprintf( stderr ,"\r                          " );}fprintf( stderr ,"\r幸运观众:%s\n" , NameS[index]);return 0;
}
  • 假如有如下定义:int a[3][5]; 完成如下要求:
  • 用1种方法表示 a[2][3] 的地址。 : &a[2][3] *(a+2)+3
  • 用3种完全等价的方法表示 a[0][0] 的地址。
  • 用2种完全等价的方法表示 a[2][0] 的地址。
//- 用1种方法表示 a[2][3] 的地址。 :  &a[2][3]   *(a+2)+3
printf("&a[2][3]:%p\n" , &a[2][3] );
printf("*(a+2)+3:%p\n" ,*(a+2)+3 );printf("**************************\n");//- 用2种完全等价的方法表示 a[2][0] 的地址。
printf("&a[2][0]:%p\n" , &a[2][0] );
printf("a[2]:%p\n" , a[2] );
printf("&a[2]:%p\n" , &a[2] );
printf("*(a+2):%p\n" ,*(a+2) );printf("**************************\n");// - 用3种完全等价的方法表示 a[0][0] 的地址。
printf("&a[0][0]:%p\n" , &a[0][0] );
printf("a[0]:%p\n" , a[0] );
printf("&a[0]:%p\n" , &a[0] );
printf("*a:%p\n" ,*a );
printf("a:%p\n" ,a );
    • 分析下面的程序的执行结果。
#include <stdio.h>
int main(void)
{int a[] = {1, 2, 3, 4};int i, *p;for(i=0, p=a; i<4; i++, p++){printf("%d %d\n", a[i], *p);}return 0;
}1 1
2 2
3 3
4 4
  • 【拓展】编写一个函数,接收三个类型相同的整型数组 a、b 和 c,将 a 和 b 的各个元素的值相加,存放到数组 c 中。
  • 【拓展】尝试使用指针来访问一维数组、二维数组中的元素。

四、数组进阶

数组名涵义

  • 数组名有两个含义:
    • 第一含义是:整个数组
    • 第二含义是:首元素地址
  • 当出现以下情形时,那么数组名就代表整个数组:
    • 在数组定义中 int arr [3] ;
    • 在 sizeof 运算表达式中 sizeof(arr) ;
    • 在取址符&中 &arr
  • 其他任何情形下,那么数组名就代表首元素地址。即:此时数组名就是一个指向首元素的指针。
  • 示例:
int a[3];                  // 此处,a 代表整个数组
printf("%d\n", sizeof(a)); // 此处,a 代表整个数组,因此计算的结果是整个数组的大小
printf("%p\n", &a);        // 此处,a 代表整个数组,此处为整个数组的地址int *p = a;       // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]  , 指针p 指向的是数组中第0个元素的地址
p = a + 1;        // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]  , 指针p 指向的是数组中第1个元素的地址
function(a);      // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0] , 传递给函数function的数组中第0个元素的地址
scanf("%d\n", a); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]

C语言只有在第一含义的场合下表现为数组,其他大部分场合都表现为首元素的地址,当数组表现为首元素地址时,实际上就是一个指向其首元素的指针。数组运算实际上就是指针运算。

数组下标

数组的下标实际上就是基于入口地址的偏移量。

int arr[10];
a[3]  --》 基于 a  往后面偏移 3个int 的数据
a[0]  
  • 数组下标实际上是编译系统的一种简写,其等价形式是:
a[i] = 100;  等价于  *(a+i) = 100;
  • 根据加法交换律,以下的所有的语句均是等价的:
  a[i] = 100;
*(a+i) = 100;
*(i+a) = 100;i[a] = 100;
  • 数组运算,等价于指针运算。

字符串('\0')常量

  • 字符串常量在内存中的存储(存储于常量区),实质是一个匿名数组
  • 匿名数组,同样满足数组两种涵义的规定
  • 示例:
printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组
printf("%p\n", &"abcd");        // 此处 "abcd" 代表整个数组printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址
char *p1 = "abcd";         // 此处 "abcd" 代表匿名数组的首元素地址
char *p2 = "abcd" + 1;     // 此处 "abcd" 代表匿名数组的首元素地址

零长数组

  • 概念:长度为0的数组,比如 int data[0];
  • 用途:放在结构体的末尾,作为可变长度数据的入口
  • 因为数组是唯一一个允许进行越界访问的接口
  • 示例:
struct node
{/* 结构体的其他成员 */// 成员1// 成员2// ... ...int   len;char *data[0];
};// 给结构体额外分配 10 个字节的内存。
struct node *p = malloc(sizeof(struct node) + 10);
p->len = 10;// 额外分配的内存可以通过 data 来使用
p->data[0] ~ p->data[9]

变长数组

  • 概念:定义时,使用变量作为元素个数的数组
  • 要点:变长数组仅仅指元素个数在定义时是变量,而绝指数组的长度可长可短实际上,不管是普通数组还是所谓的变长数组,数组一旦定义完毕,其长度则不可改变
    • 只有在定义之前数组的大小是不确定的, 一旦定义结束后数组的大小就固定下来,与变量不在有任何的关系
  • 示例:
int len = 5;
scanf("%d" , &len) ;//99
int a[len];  // 数组元素个数 len 是变量,因此数组 a 是变长数组
len = 99 ; // 不再影响数组的大小int x = 2;
int y = 3;
int b[x][y]; // 数组元素个数 x、y 是变量,因此数组 b 是变长数组
int b[2][y]; // 数组元素个数 y 是变量,因此数组 b 是变长数组
int b[x][3]; // 数组元素个数 x 是变量,因此数组 b 是变长数组
  • 语法:变长数组不可初始化,即以下代码是错误的:
int len = 5;
int a[len] = {1,2,3,4,5}; // 数组 a 不可初始化

五、指针进阶

char型指针

char型指针实质上跟别的类型的指针并无本质区别,但由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。

  • 定义:
char *p = "abcd";  // 变量p存放的是匿名数组"abcd" 的入口地址 == ‘a’的地址

多级指针

  • 如果一个指针变量 p1 存储的地址,是另一个普通变量 a 的地址,那么称 p1 为一级指针
  • 如果一个指针变量 p2 存储的地址,是指针变量 p1 的地址,那么称 p2 为二级指针
  • 如果一个指针变量 p3 存储的地址,是指针变量 p2 的地址,那么称 p3 为三级指针
  • 以此类推,p2、p3等指针被称为多级指针

  • 示例:
int a = 100;
int   *p1 = &a;  // 一级指针,指向普通变量
int  **p2 = &p1; // 二级指针,指向一级指针
int ***p3 = &p2; // 三级指针,指向二级指针

指针万能拆解法

  • 任意的指针,不管有多复杂,其定义都由两部分组成。
    • 第1部分:指针所指向的数据类型,可以是任意的类型
    • 第2部分:指针的名字

  • 示例:
char   (*p1);      // 第2部分:*p1; 第1部分:char; 
char  *(*p2);      // 第2部分:*p2; 第1部分:char *; 
char **(*p3);      // 第2部分:*p3; 第1部分:char **; 
char   (*p4)[3];  // 第2部分:*p4; 第1部分:char [3]; 
char   (*p5)(int, float); // 第2部分:*p5; 第1部分:char (int, float); 
  • 注解:
  1. 上述示例中,p1、p2、p3、p4、p5本质上并无区别,它们均是指针
  2. 上述示例中,p1、p2、p3、p4、p5唯一的不同,是它们所指向的数据类型不同
  3. 第1部分的声明语句,如果由多个单词组成,C语言规定需要将其拆散写到第2部分的两边

void型指针

int   * p  ; //  *p  把p所指向的内存中的二进制码通过【整型的规则】进行解析的到数据
char  * p1 ; //  *p1  把p1所指向的内存中的二进制码通过【字符型的规则】进行解析的到数据
float * p2 ; //  *p2  把p2所指向的内存中的二进制码通过【浮点的规则】进行解析的到数据
void  * p3 ; //  *p3  把p3所指向的内存中的二进制码通过 【不知道什么规则】 进行解析的到数据// 因此 *p3 是不合理的,没有具体的规则解析内存则无法解析 ---》 无法直接使用 void 指针
  • 概念:无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针 , 一般用于函数的返回值。
  • 要点:
    1. void 型指针无法直接索引目标必须将其转换为一种具体类型的指针方可索引目标(转换类型后则有了具体解析内存的规则)
    2. void 型指针无法进行加减法运算 (指针的加减是根据指针的类型进行地址偏移操作)
  • void关键字的三个作用:
    1. 修饰指针,表示指针指向一个类型未知的数据。
    2. 修饰函数参数列表,表示函数不接收任何参数
    3. 修饰函数返回类型,表示函数不返回任何数据
  • 示例:
// 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
void *p = malloc(4); // malloc (4) 在堆内存中申请4个字节的内存空间并返回// 1,将这 4 字节内存用来存储 int 型数据
*(int *)p = 100; // 先使用(int *) 来强制类型转换把p的类型转为 int *。然后才能*解引用
printf("%d\n", *(int *)p);// 2,将这 4 字节内存用来存储 float 型数据
*(float *)p = 3.14;
printf("%f\n", *(float *)p);

void *一般用于函数的返回值,比如:

        以上函数都是用于在堆中申请内存的操作接口,由于申请到的内存可能会用于存储任何类型的数据,因此在设计这类申请内存的函数的时候不应该明确具体内存的地址类型,否则可能需要设计N个返回不同类型的申请函数。

野指针

  • 概念:指向一块未知区域的指针,被称为野指针。野指针是危险的

  • 危害:
  1. 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
  2. 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
  • 产生原因:
  1. 指针定义之后,未初始化
  2. 指针所指向的内存,被系统回收
  3. 指针越界
  • 如何防止:
  1.  指针定义时,及时初始化
  2. 绝不引用已被系统回收的内存
  3. 确认所申请的内存边界,谨防越界

空指针

很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。

  • 概念:空指针即保存了零地址的指针,亦即指向零地址的指针
  • 示例:
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int  *p2 = NULL;
float *p3 = 0 ;// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3);               // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL;              // c. 让 p3 指向零地址

指针的加减:

  • 指针加法意味着地址向上(高地址)移动若干个目标
  • 指针减法意味着地址向下(低地址)移动若干个目标
  • 示例:
int  a = 100;
int *p = &a; // 指针 p 指向整型变量 aint *k1 = p + 2; // 向上移动 2 个目标(2个int型数据)
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)

long long   a ;char * p = &a ;printf("p:%p\n" , p);printf("p+1:%p\n" , p+1);printf("p+2:%p\n" , p+2);printf("p-1:%p\n" , p-1);printf("p-2:%p\n" , p-2);
  • 总结:
    • 指针的加减运算时 它加减的目标 大小是以指针的类型为单位,
      • 如果指针指向的是一个整形地址则+1、-1的时候以 int 为单位进行加减,
      • 如果指针的类型是Long 类型则 则+1、-1的时候以 long 为单位进行加减
    • 指针的加减与指针所指向的数据本身的类型没有任何关系(比如以上代码a不管是什么类型都不影响p的运算)

拓展:

long long  ** p1 = &a ;
int  ** p2 ;
char ** p3 ;

以上代码中p是一个二级指针,因此可以理解为: *p 明确了变量p是一个指针,剩下的 long long * 表示该变量存储的类型是一个 long long类型的地址, 由于地址的大小在某一个系统中是固定的,随意这个指针p在进行加减运算的时候步伐与地址的大小相关。

以上 p1 \p2 \p3 加减时都是以8字节(64为系统 ) 、 4 字节 (32为系统)为单位。

const 型指针

  • const型指针有两种形式:①常指针 ②常目标指针
  1. 常指针const修饰指针本身,表示指针变量本身无法修改(指针的指向是固定的)。

  1. 常目标指针const修饰指针的目标,表示无法通过该指针修改其目标

  • 常指针在实际应用中不常见。
  • 常目标指针在实际应用中广泛可见,用来限制指针的读写权限
  • 示例:
int a = 100;
int b = 200;// 第1中形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a;   // p1 的指向无法被修改它将“永远”指向 a 的地址// 【拓展】 从侧面修改p1的指向
int ** p4 = &p1 ;
*p4 = &b ;  // 可以通过二级指针来间接修改p3所指向的内容
printf("*p3: %d \n" , *p3);// 第2中形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;  // 无法通过 p2 来修改 变量 a 的数据
const int *p2 = &a;

常目标常指针

const  int * const ptr = &a ;
ptr = &b ; // [不允许]
*ptr = 777 ; // [不允许]

函数指针

  • 概念:指向函数的指针,称为函数指针
  • 特点:函数指针跟普通指针本质上并无区别,只是在取址和索引时,取址符和星号均可省略
  • 语法:

返回值类型 (*p) (参数列表) ;

  • 示例:
void   f (int) // 函数 f 的类型是: void (int)
{}void (*p)(int); // 指针 p 专门用于指向类型为 void (int) 的函数p = &f; // p 指向 f(取址符&可以省略)
p =  f; // p 指向 f// 以下三个式子是等价的:f (666); // 直接调用函数 f
(*p)(666); // 通过索引指针 p 的目标,间接调用函数 fp (666); // 函数指针在索引其目标时,星号可以省略

注意:

    1. 函数指针是一类专门用来指向(存储)某种类型函数的指针(地址)。
    2. 函数的类型不同,所需要的函数指针也不同。
    3. 函数的类型,与普通变量的类型判定一致,即去除声明语句中的标识符之后所剩的语句。

总结

        本文细讲了打怪路上的数组和指针的特点和消灭方法,各位只需认真学习,即可消灭它们。祝各位都可爬上C语巅峰,斩尽拦路小妖。

        本文参考 粤嵌文哥 的部分课件,经过整理和修改后发布在C站。如有转载,请联系本人

相关文章:

  • 电商项目之Web实时消息推送(附源码)
  • 类和对象——(7)this指针
  • 5G常用简称
  • 初识Linux——基本指令(详解)1
  • Centos7.4安装nginx1.24.0_安装详细步骤---Linux工作笔记066
  • shell读取python版本
  • Ubuntu18.04 本地安装CVAT标注工具
  • Python 调用企业微信群机器人发送消息及文件
  • AI助力智慧农业,基于YOLOv5全系列模型【n/s/m/l/x】开发构建不同参数量级农田场景下庄稼作物、杂草智能检测识别系统
  • Linux下的java环境搭建
  • 【无标题】从0到1 搭建一个vue3+Django项目
  • 力扣二叉树--第三十九天
  • 线性表之-栈
  • 第三节:提供者、消费者、Eureka
  • STM32F1中断NVIC
  • 【译】React性能工程(下) -- 深入研究React性能调试
  • android图片蒙层
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • CSS选择器——伪元素选择器之处理父元素高度及外边距溢出
  • ES6语法详解(一)
  • js数组之filter
  • js学习笔记
  • MYSQL 的 IF 函数
  • nodejs:开发并发布一个nodejs包
  • Service Worker
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • VUE es6技巧写法(持续更新中~~~)
  • Vue 重置组件到初始状态
  • 干货 | 以太坊Mist负责人教你建立无服务器应用
  • 两列自适应布局方案整理
  • 聊聊directory traversal attack
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 山寨一个 Promise
  • 学习Vue.js的五个小例子
  • ionic异常记录
  • 阿里云重庆大学大数据训练营落地分享
  • 直播平台建设千万不要忘记流媒体服务器的存在 ...
  • ​secrets --- 生成管理密码的安全随机数​
  • #周末课堂# 【Linux + JVM + Mysql高级性能优化班】(火热报名中~~~)
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (10)ATF MMU转换表
  • (70min)字节暑假实习二面(已挂)
  • (C语言)二分查找 超详细
  • (C语言)逆序输出字符串
  • (安卓)跳转应用市场APP详情页的方式
  • (补)B+树一些思想
  • (附源码)ssm高校升本考试管理系统 毕业设计 201631
  • . Flume面试题
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .Net Core缓存组件(MemoryCache)源码解析
  • .NET 使用配置文件
  • .NET值类型变量“活”在哪?
  • @Autowired和@Resource的区别
  • [ARM]ldr 和 adr 伪指令的区别
  • [AutoSAR系列] 1.3 AutoSar 架构