C语言指针进阶(上)

简介: C语言指针进阶(上)

前言

1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间

2.指针的大小是4或者8个字节(32位平台4个字节,64位平台8个字节)

3.指针是有类型的,指针的类型决定了指针±一个整数的长度有多少,指针解引用操作时候的权限

4.指针的运算

指针能够加减整数运算

指针能够解指针

指针能够进行关系运算,也就是地址的关系运算,比较大小

一、字符指针

字符指针不仅仅可以指向字符,还可以指向字符串.

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main() {
  char ch = 'w';
  char* pc = &ch;//pc就是字符指针
  //1.
  char* pd = "abcdef";  //"abcdef"表达式的值是首字符a的地址,p中实际上放的是a的地址
             //常量字符串不能修改
  //2.
  char arr[] = "abcdef";
  char* pf = arr;   //pf指向的是数组首元素的地址
            //arr数组是可以修改的
  //修改
  *pf = 'w';
  printf("%s\n", arr);
  return 0;
}

注意:

在有一些编译器中下面的代码会报错,因为"abcdef"是常量字符串,本质上不能修改,将这个字符串交给p是很危险的,char p 没有保护起来,所以通过p万一去修改a,在运行的时候就会出错.

char* p="abcdef";
*p='w';

解决办法:

加上const,意思是const修饰的是p,限制了p,

此时*p不能被修改

const char* p="abcdef";

所以即使是在vs2019中可以运行,但最好也在前面加上const

面试题讲解

#include <stdio.h>
int main()
{
  char str1[] = "hello world.";
  char str2[] = "hello world.";
  const char* str3 = "hello world.";
  const char* str4 = "hello world.";
  if (str1 == str2)
    printf("str1 and str2 are same\n");
  else
    printf("str1 and str2 are not same\n");
  if (str3 == str4)
    printf("str3 and str4 are same\n");
  else
    printf("str3 and str4 are not same\n");
  return 0;
}

分析:

拿"hello world."分别初始化一个数组,是必然都会创建的.创建两个数组在内存中的位置一定是有区别的.所以str1和str2不相等

对于const 修饰的常量字符串,不能被改,没有必要存两份,所以str3和str4里面存的是一样的

该题拓展:

下面的代码相等吗?

if(&str3==&str4){
}

答案:不相等.因为变量str3和str4是不在一个空间里面的,取str3的地址和str4的地址肯定不相等.

与上面的区别是:数组名是数组里面首元素的地址,而&str3取得是变量str3的地址

二、指针数组

指针数组:存放的是指针的数组

存放字符指针的数组(字符指针数组)

char* arr3[5];

存放整型指针的数组(整型指针数组)

int* arr[6];

1.字符指针数组

int main() {
  char* arr[] = { "abcdef","hehe","qwer" };
  for (i = 0; i< 3; i++) {
    printf("%s\n", arr[i]);
  }
  return 0;
}

2.整型指针数组

int main() {
  int arr1[] = { 1,2,3,4,5 };
  int arr2[] = { 2,3,4,5,6 };
  int arr3[] = { 3,4,5,6,7 };
  //数组首元素地址是整型地址
  //arr是一个存放整型指针的数组
  int* arr[] = { arr1,arr2,arr3 };
  int i = 0;
  for (i = 0; i < 3; i++) {
    //arr[i]拿到的是arr1/arr2/arr3的地址
    //加上一个j再解引用就可以拿到里面的各个元素
    int j = 0;
    for (j = 0; j < 5; j++) {
      //arr[i][j]也可以写成arr[i]+j
      printf("%d ", arr[i][j]);
      //printf("%d ", *(arr[i] + j));
    }
    printf("\n");
  }
  return 0;
}

三、数组指针

类比上面的定义:

1.整型指针:指向整型的指针

int a = 20;
  int* p = &a;

2.字符指针:指向字符的指针

char ch = 'w';
  char* pc = &ch;

3.数组指针:指向数组的指针

数组名是首元素的地址,有两个例外:

一是sizeof内部单独放一个数组名

另一个是取地址数组名,这个取出的是数组的地址

int arr[10];

&arr; //取出的是数组的地址

pa说明pa是指针,当去掉pa时剩下int [10],说明pa指向的是数组

int arr[10];
  int(*pa)[10] = &arr;
char arr2[10];
  char(*pc)[10] = &arr2;

4.注意区分:arr 和&arr

绝大多数情况下,数组名是数组首元素的地址

但是有两个例外:

1.sizeof(数组名) sizeof内部单独放一个数组名的时候,数组名表示整个数组,计算得到的是数组的总大小

2.&arr 这里的数组名表示整个数组,取出的是整个数组的地址,从地址值的角度来讲和数组首元素的地址一样,但是意义不一样

int main() {
  int arr[10] = { 0 };
  printf("%p\n", arr);
  printf("%p\n", &arr[0]);
  printf("%p\n", &arr);
  return 0;
}

int main() {
  int arr[10] = { 0 };
  printf("%p\n", arr);
  printf("%p\n", arr+1);
  printf("%p\n", &arr[0]);
  printf("%p\n", &arr[0]+1);
  printf("%p\n", &arr);
  printf("%p\n", &arr+1);
  return 0;
}

printf(“%p\n”, arr);

printf(“%p\n”, arr+1);

数组名+1,数组名的类型是int*,int类型的指针+1之后跳过的是4个字节
printf(“%p\n”, &arr[0]);
printf(“%p\n”, &arr[0]+1);
&arr[0]的类型也是int
,int*类型的指针+1之后跳过的是4个字节

printf(“%p\n”, &arr);

printf(“%p\n”, &arr+1);

&arr的类型是指向数组的指针,它的类型是int( * )[10],+1的时候跳过整个数组的长度

5.访问数组的三种形式

(1)下标访问

(2)指针访问

(3)数组指针访问(不推荐)

int main() {
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int i = 0;
  int sz = sizeof(arr) / sizeof(arr[0]);
  //使用指针来访问
  int* p = arr;
  for (i = 0; i < sz; i++) {
    printf("%d ", *(p + i));
  }
  //下标访问的形式访问数组
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  //取出整个数组的地址,与上面访问数组的结果相同,但是不易于理解,不推荐
  int(*pa)[10] = &arr;    //pa相当于&arr  *pa相当于*&arr也就是*pa=arr  
  for (i = 0; i < sz; i++) {
    printf("%d ", *((*pa) + i));//*pa是首元素的地址
    //printf("%d ", (*pa)[i]);与上面等同
  }
}

6.数组传参

(1)一维数组传参,形参写成数组或者指针的形式

I.写成数组的形式
void print(int arr[10],int sz){
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main() {
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(arr, sz);
}
II. 写成指针的形式
void print(int *arr, int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main() {
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(arr, sz);
}

(2)二维数组传参

I.形式参数写成数组的形式
void print(int arr[3][5],int r,int c) {
  int i = 0;
  for (i = 0; i < 3; i++) {
    int j = 0;
    for (j = 0; j < 5; 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 };
  //封装一个函数打印二维数组
  //需要传数组,行和列
  print(arr, 3,5);
  return 0;
}

II.形式参数写成指针的形式

二维数组的数组名也表示首元素的地址.

二维数组其实是一维数组的数组.

每一行都是一个元素

二维数组的首元素是?

二维数组的首元素是第一行

首元素的地址就是第一行的地址

第一行的地址是一个一维数组的地址

形式参数要用数组指针int (*arr)[5]

//二维数组传参,形式参数用指针表示
void print(int (*arr)[5], int r, int c) {
  int i = 0;
  for (i = 0; ![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/d69157ed8d1c428ea0d1a878b4d476c0.png)
i < 3; i++) {
    int j = 0;
    for (j = 0; j < 5; j++) {
      //printf("%d ",  *(*(arr+i)+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 };
  //封装一个函数打印二维数组
  //需要传数组,行和列
  print(arr, 3, 5);
  return 0;
}

注意:

7.分析代码

(1)

int *parr1[10];

parr1和[]结合,是数组,十个元素,每个元素是int*类型,所以它是指针数组

(2)

int(*parr2)[10];

parr2是一个指针,指向的是数组,数组十个元素每个元素是int类型,所以它是数组指针

(3)

int(*parr3[10])[5];

parr3首先和[]结合是数组,数组名和数组个数parr3[10]除外,剩下的就是数组元素的类型int( * )[5],int( * )[5]说明是指针,指针指向的是5个元素的数组,每个元素的类型是int

parr3是数组,数组中存放指针,该指针又指向数组

可以理解为指针数组存放的是数组指针

四、数组参数、指针参数

1.一维数组传参

int main() {
  int arr1[10] = { 0 };
  int *arr2[20] = { 0 };
  test(arr1);
  test2(arr2);
}

以下test的写法可行吗?

(1)

void test(int arr[]) {
}

ok.

数组的大小可以不写,因为虽然这里传过来的是一个数组,但是不会在函数里面真的创建一个数组,不需要大小

(2)

void test(int arr[10]) {
}

ok

形参的数组可以写大小

(3)数组名传过去,形参的部分写成指针行不行?

void test(int *arr) {
}

行还是不行取决于传过去的是什么

数组名表示首元素的地址,首元素是int的地址,整型的地址就要放到整型的指针里面

ok

以下test2的写法可行吗?

(1)

void test2(int* arr[20]) {
}

arr2数组名表示首元素的地址,一维数组传参,形参部分写成数组,可行

数组20个元素,每个元素是int*类型

ok

(2)

能不能这样写?

void test2(int **arr) {
}

因为这个数组每个元素都是int*,数组名表示首元素地址,就是一个int*的地址(一级指针的地址),应该放到二级指针变量里边去

ok

二级指针简单说明:

二级指针是指向指针的指针.

它的作用是为了获取指针的存放地址.

首先任何值都有地址,一级指针的值虽然是地址,但这个地址作为一个值也需要空间来存放,二级指针就是来存放这一空间的地址.

结论:

(1)一维数组传参,形参可以是数组,也可以是指针的.

(2)当形参是指针的时候,要注意类型

2.二维数组传参

int main() {
  int arr[3][5] = { 0 };
  test(arr);
}

形参的部分可以这样写吗?

(1)

void test(int arr[3][5]) {
}

ok

二维数组传参还写成二维数组的形式,3行5列,没问题

(2)可以省去二维数组形参的行和列的长度吗?

void test(int arr[][]) {
}

不行!

行可以省略,列不可以省略.

因为需要知道一行有几个元素,但是有几行不关心,只有找完一行才能找到下一行.

二维数组的内存是连续存放的.只有知道一行有几个元素的时候,才能知道第二行在哪里,第三行在哪里.

应该写成:

void test(int arr[][5]) {
}

(3)

void test(int* arr) {
}

不行!

二维数组首元素的地址相当于是它第一行的地址,第一行是五个整型的数组.相当于首元素的地址是一维数组首元素的地址,这里形参写一个整型指针来接收是不行的.

(4)

void test(int* arr[5]) {
}

不行!

形参写成了数组,数组的每个元素是int*,完全搭不上

二维数组传参,要么写成二维数组的形式,要么写成指针的形式,这里写的是指针数组显然是不对的

(5)

void test(int(*arr)[5]) {
}

ok

二维数组首元素的地址是一维数组首元素的地址,要用5个整型的数组指针来接收

(6)

形参的部分可以写成二级指针的形式吗?

void test(int **arr) {
}

因为传过去的就不是一个一级指针,二级指针是用来接收一级指针的地址

这个写法完全不对,传过去的是一行的地址拿二级指针接收,完全不搭配

结论:

二维数组传参,参数可以是指针,也可以是数组.

如果是数组,行可以省略,但列不能省略

如果是指针,传过去的事第一行的地址,形参就应该是数组指针

3.一级指针传参

指针在传参的时候,类型只要匹配上,没有任何问题

一级指针传过去,形参的部分用一级指针接收

void print(int* p, int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d\n", *(p + i));
  }
}
int main() {
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int* p = arr;  //1的地址交给了p
  int sz = sizeof(arr) / sizeof(arr[0]);
  //一级指针p,传给函数
  print(p, sz);
  return 0;
}

思考:

当一个函数的形参部分为整型的一级指针的时候,函数能接收什么参数?

(1)传一个整型变量的地址

int a;
print(&a,10);

(2)传一个整型指针

int a;
int *p1=&a;
print(p1,10);

(3)传整型数组的数组名

int arr[10];
print(arr,10);

4.二级指针传参

二级指针传参的时候,形参部分就用二级指针接收

void test(int** ptr) {
  printf("num=%d\n", **ptr);
}
int main() {
  int n = 10;
  int* p = &n;
  int** pp = &p;
  test(pp);
  test(&p);
  return 0;
}

pp二级指针传参的时候,形参的部分直接写成二级指针就可以了

&p是二级指针的地址,传过去应该用二级指针变量接收

思考:

当函数的参数是二级指针的时候,可以接收什么参数?

比如:

当只有下面这个函数的时候,调用它的时候可以传什么?

void test(int** ptr) {
  printf("num=%d\n", **ptr);
}

(1)传一个二级指针变量

(2)传一个一级指针变量的地址

(3)传一个指针数组

因为数组名表示首元素的地址,数组的每个元素都是int*,首元素的地址就是int*的地址,也就是二级指针,这里用二级指针接收,没问题

int* arr[10];
test(arr);

五、函数指针

1.回顾一下指针的类型

整型指针-指向整型的指针 int*

字符指针-指向字符的指针 char*

数组指针-指向数组的指针

int arr[10]; int (*p)[10]=&arr;

函数指针-指向函数的指针

数组指针中存放的是数组的地址

函数指针中存放的是函数的地址

&函数名 得到的就是函数的地址

数组:

数组名:首元素的地址

&数组名:数组的地址

函数:

函数名和&函数名,都是函数的地址,没有区别

int Add(int x, int y) {
  return x + y;
}
int main() {
  printf("%p\n", &Add);
  printf("%p\n", Add);
  return 0;
}

2.函数指针变量用来存放函数的地址

pf是函数指针的名字,去掉它是函数指针的类型,不需要交代形参的名字

int Add(int x, int y) {
  return x + y;
}
int main() {
  int (*pf)(int,int) = Add;//pf是函数指针的名字,去掉它是函数指针的类型,不需要交代形参的名字
  return 0;
}

3.通过函数指针调用它指向的函数

int Add(int x, int y) {
  return x + y;
}
int main() {
  int (*pf)(int,int) = Add;//pf是函数指针的名字,去掉它是函数指针的类型,不需要交代形参的名字
  int ret = (*pf)(3, 5);  //调用
  printf("%d\n", ret);
  return 0;
}

pf是函数指针的名字,去掉它是函数指针的类型.把Add放到pf里边,Add和pf就是一回事

总结:

(1)函数指针调用:

int ret = (*pf)(3, 5);

在看完下面的(2)(3)之后发现这里的*其实就是一个摆设,那为什么既然是一个摆设,还能用呢?

其实就是为了方便理解,通常来说,pf是一个指针,要访问指针指向的对象需要解引用一下找到这个函数,然后再去调用它.对于编译器来说写或者不写都无关紧要.多写几个也无所谓,不会影响.

但是如果写的话,一定要把它和函数指针变量括起来,然后找到那个函数再调用它.

(2) 曾经调用函数使用:拿到函数名就是函数地址,再传参就可以调用,把结果放到ret里边去

int ret=Add(3,5);

(3)同理,Add和pf一回事,写成下面的形式也可以:

int ret=pf(3,5);

上述知识考察

下面这个函数的地址怎么用函数指针存起来?

char* test(int c,float* pf){
}

解答:

int main()
{
char* (*pf)(int,float*)=test;
return 0;
}

4.阅读两段有趣的代码

下面的代码是什么意思呢?

代码1

int main() {
  (*(void(*)())0)();
  return 0;
}

(1)

从0开始找切入口,0前面的括号里面是void( * )(),void( * )()是函数指针的类型.

(2)

类型放到括号里面是什么意思?是要进行强制类型转换.

0本来是int类型的,在前面放一个括号意思是强制类型转换,也就是把0转换成一种指针类型(在这里是函数指针类型),0也就转换成了函数地址

(4)

前面有一个*,也就是对强转换的地址进行解引用---->调用这个函数,调用的时候要传参,所以在最后有一个圆括号,调用的时候没有传参

代码1的意思:

I.将0强制类型转成void(*)()类型的函数指针

II.这就意味着0地址处放着一个函数,函数没有参数,返回类型是void

III.调用0地址处的这个函数

代码2:

int main() {
  void(*signal(int, void(*)(int)))(int);
  return 0;
}

理解:

函数的名字是signal,第一个参数是int类型,第二个参数是函数指针类型,去掉signal(int, void()(int)),剩下的void( )(int)即函数的返回类型是函数指针

代码2的意思:

上面的代码是一个函数声明

函数的名字是signal

函数的参数第一个是int类型,第二个类型是void(*)(int)类型的函数指针

该函数指针指向的函数参数是int,返回类型是void

signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

代码2不易于理解,用typedef简化一下

typedef可以把某些类型进行简化

typedef int* ptr_t; //将int*取别名为ptr_t
//将void(*)(int)类型的函数指针重新起个别名叫pf_t
//写成typedef void(*)(int) pf_t是不符合语法的,重命名必须放在*的旁边
typedef void(*pf_t)(int);  
pf_t signal(int, pf_t);

注意:

typedef void(*pf_t2)(int);  //pf_t2是类型名
void(*pf)(int);    //pf是函数指针变量的名字

一般对类型重定义用typedef,不用#define

六.函数指针数组

指针数组:整型指针的数组

函数指针数组:数组的每个元素是一个函数指针

int Add(int x,int y) {
  return x + y;
}
int Sub(int x, int y) {
  return x - y;
}
int Mul(int x, int y) {
  return x * y;
}
int Div(int x, int y) {
  return x / y;
}
int main() {
  //同样类型的变量要定义四次非常繁琐,用数组统一保存
  int(*pf1)(int, int) = Add;
  int(*pf2)(int, int) = Sub;
  int(*pf3)(int, int) = Mul;
  int(*pf4)(int, int) = Div;
  //存放函数指针的数组---函数指针数组
  int(*pf[4])(int, int) = {Add,Sub,Mul,Div};
  //数组里面存放四个元素,每个元素是函数指针类型的数据,存的是四个函数的地址
  //下标:0 1 2 3
  //找到这四个函数
  int i = 0;
  for (i = 0; i <4; i++) {
    int ret = pf[i](8, 4);   //pf[i]代表的是数组里面各个元素,再传参,用ret接收结果
    printf("%d\n", ret);
  }
  return 0;
}

知识点练习

写一个计算器能完成整数的±*/

void menu() {
  printf("***************************************\n");
  printf("******1.add 2.sub**********************\n");
  printf("******3.mul 4.div**********************\n");
  printf("******0.exit     **********************\n");
  printf("***************************************\n");
}
int Add(int x, int y) {
  return x + y;
}
int Sub(int x, int y) {
  return x - y;
}
int Mul(int x, int y) {
  return x * y;
}
int Div(int x, int y) {
  return x / y;
}
int main() {
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  do {
    menu();
    printf("请选择->");
    scanf("%d", &input);
    //只有输入1234的时候才需要打印结果
    //为了避免无关紧要的一些结果的输出
    switch (input) {
    case 1:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Add(x, y);
      printf("%d\n", ret);
      break;
    case 2:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Sub(x, y);
      printf("%d\n", ret);
      break;
    case 3:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Mul(x, y);
      printf("%d\n", ret);
      break;
    case 4:
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Div(x, y);
      printf("%d\n", ret);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    default:
      printf("选择错误\n");
      break;
    } 
  }while (input);
  return 0;
}

在计算机基础上, 增加一些其他的功能:>> << & | ^ && ||

一直case下去代码太长了,有没有简化的方法?

将上面的代码简化

void menu() {
  printf("***************************************\n");
  printf("******1.add 2.sub**********************\n");
  printf("******3.mul 4.div**********************\n");
  printf("******0.exit     **********************\n");
  printf("***************************************\n");
}
int Add(int x, int y) {
  return x + y;
}
int Sub(int x, int y) {
  return x - y;
}
int Mul(int x, int y) {
  return x * y;
}
int Div(int x, int y) {
  return x / y;
}
int main() {
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  //转移表
  //先输入一个下标,通过下标找到对应的函数,然后调用这个函数
  //有一种跳转的效果
  //注意:放进指针数组的函数的指针类型要保持一致
  //NULL就是0,0就是NULL,这里起到的是占位的作用
  int (*pfArr[])(int, int) = {NULL,Add,Sub,Mul,Div};  //创建一个函数指针的数组
                      //下标   0  1   2   3   4
  do {
    menu();
    printf("请选择->");
    scanf("%d", &input);
    if (input >= 1 && input <= 4) {   //这里的4可以写成sizeof(pfArr)/sizeof(pfArr[0]再-1)
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = pfArr[input](x, y); //pfArr[i]找到的是数组里面元素的地址,找到之后传参数x,y,调用函数
      printf("%d\n", ret);
    }
    else if (input == 0) {
      printf("退出计算器\n");
      break;
    }
    else {
      printf("选择错误\n");
    }
  } while (input);
  return 0;
}

注意点:


总结

💕

指针进阶的上部分内容就到这里啦,如果对友友们有帮助的话,记得点赞收藏博客,关注后续的指针进阶下集内容哦~👻👻👻

相关文章
|
4天前
C进阶:指针的练习(1)
C进阶:指针的练习(1)
|
4天前
C进阶:指针的进阶(4)
C进阶:指针的进阶(4)
|
4天前
|
存储 C语言
C进阶:指针的进阶(1)
C进阶:指针的进阶(1)
|
4天前
|
C语言 索引
C语言指针详解(4)
C语言指针详解(4)
|
4天前
|
存储 编译器 C语言
经典指针与数组笔试题——C语言
经典指针与数组笔试题——C语言
|
1天前
|
存储 C语言 C++
C语言指针详解进阶1
C语言指针详解进阶1
10 1
|
1天前
|
存储 安全 编译器
C语言指针详解
C语言指针详解
|
4天前
|
存储
C进阶:指针的进阶(3)
C进阶:指针的进阶(3)
|
4天前
|
C++
C进阶:指针的进阶(2)
C进阶:指针的进阶(2)
|
4天前
|
算法 编译器 C语言
C语言assert断言详解指针(3)
C语言assert断言详解指针(3)

相关产品

  • 云迁移中心