C语言进阶-指针进阶(1)

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

我们在之前学习指针初阶的时候就知道了有关指针的概念:

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

2.指针的大小固定为4/8个字节(32位平台/64位平台)

3.指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作时的权限。

4.指针的运算

下面我们先来回顾一下之前的学习内容,

指针:内存会划分为一个个的内存单元,每个内存单元都有有一个独立编号,这个编号也称为地址,地址在C语言中被称为指针,指针(地址)需要被存储起来,存储变量中,这个变量就被称为指针变量。

为什么指针的大小固定为4/8位(32位平台/64位平台)?

因为地址是物理的电线上产生,32位机器有32根地址线,这32而根地址线上传递0/1电信号,32个0/1组成的二进制序列作为地址。要32个bit位才能存储这个地址,也就是需要4个字节才能存储这个地址,所以指针变量的大小就是4个字节。同理,在64位机器上,地址的大小是64个0/1组成的二进制序列,需要64个bit位存储,所以指针变量的大小就是8个字节。

以上就是初阶指针中学习过的内容,下面我们来学习新的内容。

1.字符指针

前面学过关于字符指针的知识,下面就是字符指针:

int main()
{
  char ch = 'w';
  ch = 'a';
  char* pa = &ch;//pa就是字符指针
  *pa = 'b';
  return 0;
}

可以直接修改ch的内容,也可以通过指针修改。

今天我们来学习字符指针的另外一种用法,

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
  char arr[] = "abcdef";
  const char* p = "abcdef";//常量字符串
  printf("%s\n", p);
  printf("%c\n", *p);
  return 0;
}

上代码很容易让人误解为将字符串放到指针变量里面去了,但是实际上指针变量p中存放的是字符串的首字符的地址(通过打印结果可以验证),这里的字符串是常量字符串,不可被修改,所以前面最好加const修饰。

上述代码中,通过%s和指针变量p就可以打印出字符串“abcdef”,这体现的是printf函数的功能,printf函数可以通过地址打印字符串,只要给出首地址,printf函数就可以通过该地址往后打印出字符串。

学了字符指针,下面我们就可以来看一道经典的笔试题:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
  char str1[] = "hello xupt.";
  char str2[] = "hello xupt.";
  const char* str3 = "hello xupt.";
  const char* str4 = "hello xupt.";
  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;
}

来看一下运行结果:

为什么是这样的结果呢?

首先,str1[ ] 和 str2[ ]是两个数组,它们在内存中有独立的空间,数组名是数组首元素的地址,str1和str2的地址不同,这就相当于将“hello xupt.”分别存了两遍。而str3和str4相同的原因是,此时的字符串是常量字符串,不可被修改,所以只在内存中存一遍,指针变量str3和str4中存放的都是字符串“hello xupt.”的首字符的地址,所以它们相同。

2.指针数组

在C初阶中我们通过类比了解过指针数组,

整型数组--存放整数的数组。

字符数组--存放字符的数组。

那指针数组就是存放指针的数组。

int*arr[10];//整型指针的数组
char*arr[4];//一级字符指针的数组
char**arr3[5];//二级字符指针的数组

我们还将过用指针数组模拟实现二维数组:

#define  _CRT_SECURE_NO_WARNINGS 1
#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* arr[3] = { arr1,arr2,arr3 };//指针数组
  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");
  }
  return 0;
}

指针数组arr中存放的三个整型指针分别是arr1,arr2,arr3,而它们各自是大小为5的一维数组的数组名,我们可以用 i 来遍历指针数组,用 j 来遍历一维数组,由此可实现对二维数组的模拟。

3.数组指针

3.1 数组指针的定义

数组指针也可以通过类比来了解,

整型指针 -- 指向整型变量的指针,存放整型变量的地址的指针变量。

字符指针 -- 指向字符变量的指针,存放字符变量的地址的指针变量。

那么数组指针就是指向数组的指针,存放数组的的地址的指针变量。

下面两个哪一个是数组指针呢?

int*p1[10];
int(*p2)[10];

很明显,p1先与[10]结合,p1[10]是数组,它里面存放的是int*型的指针,这就是指针数组,而(*p2)是分离出来的,它的外面是int [10],是数组的类型,所以p2是指针,指向的是数组,即p2是数组指针变量。

这里我们可以总结区分一下数组指针和指针数组:

数组指针,是指针,是指向数组的指针

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

了解了数组指针,那我们要如何个数组指针变量赋值呢?

在此之前,我们要再来复习一下关于数组名的知识。

3.2 &数组名VS数组名

前面我们学过数组名就是数组首元素的地址,下面可以验证一下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);
  printf("%p\n", &arr[0]);
  return 0;
}

运行结果:

这时有人要提出疑问了,你说数组名是数组首元素的地址,那sizeof(arr)打印出来的结果应该是4啊,为什么我打印出来的是40呢?

这就不得不提起关于数组名的两个例外了:

1.sizeof(数组名)数组名不是数组首元素的地址,数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节。

2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址。

除此之外,所有地方的数组名都是数组首元素的地址。

现在我们来打印一下&arr,

此时有人有疑惑了,不是说&arr取出的是整个数组的地址吗,为什么它们三个结果是一样的?

这是因为,在内存中&arr要取出整个数组的地址也是从数组起始的地址开始的,所以以上代码看不出区别,我们可以打开监视查看:

这里就可以看到,前两个的类型都是int*型,而&arr的类型是int[10]*,这种写法不正确,但这实际上就是数组指针, 由此可见&arr取出的就是整个数组的地址。

下面我们也可以换一种方法来验证一下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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;
}

运行结果:

我们知道整型的地址通过+1操作,每次跳过4个字节,但是对&arr+1跳过了40个字节,这也可以说明&arr取出的就是整个数组的地址。

3.3数组指针的使用

到这里我们知道了&arr取出的是数组的地址,而前面说过数组指针是存放数组的地址的指针变量,那我们就可以把&arr用数组指针存起来。

int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int(*p)[10] = &arr;//数组的地址,存储到数组指针变量
  return 0;
}

注意:其中&arr的类型是 int (*) [10],刚刚前文编译器中显示的类型 int [10]*写法是错误的

我们也可以通过数组指针来访问数组元素,

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int(*p)[10] = &arr;//数组的地址,存储到数组指针变量
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", *((*p) + i));
  }
  return 0;
}

运行结果:1 2 3 4 5 6 7 8 9 10

p中存放的是&arr,*p可以得到arr,然后*(arr+i)就可以访问数组元素。

其实这里*p就相当于arr,我们也可以通过(*p)[ i ],来访问数组元素。

上面使用数组指针看起来十分别扭,实际上的数组指针也不是这么使用的,它一般在二维数组上使用。

前面我们在写一个具有打印二维数组功能的函数时,是这样写的:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
Print(int arr[3][5], int r, int c)
{
  int i = 0;
  for (i = 0; i < r; i++)
  {
    int j = 0;
    for (j = 0; j < c; 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;
}

运行结果:

而学了数组指针后,我们可以这样写:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
Print(int (*p)[5], int r, int c)
{
  int i = 0;
  for (i = 0; i < r; i++)
  {
    int j = 0;
    for (j = 0; j < c; j++)
    {
      printf("%d ", *(*( p + 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;
}

分析一下上述数组指针的用法:

我们说可以将二维数组的每一行看做二维数组的一个元素,每一行又是一个一维数组,所以,二维数组其实是一维数组的数组,而二维数组的数组名也是数组首元素的地址,所以我们在数组传参时传的arr是二维数组首元素的地址,即第一行的地址,即一维数组的地址,既然是数组的地址,我们在接收时,形参就可以用数组指针。

在访问数组元素时用*(*( p + i ) + j),其中数组指针变量p中存放的是二维数组第一行的地址,通过 *(p + i)得到每一行首元素的地址,然后通过*(p + i) + j 得到每一行每个元素的地址,最后整体解引用  :      *(*(p + i) + j)就可以访问二维数组中的元素。

4.数组参数、指针参数

4.1一维数组传参

一维数组传参,形参部分可以是数组,也可以是指针:

void test1(int arr[10], int sz)
{}
void test2(int* p, int sz)
{}
int main()
{
  int arr[10] = { 0 };
  test1(arr, 10);
  test2(arr, 10);
  return 0;
}

其实写成数组形式的int arr[10]本质上还是指针int*p

以上就是一维数组传参,看下面一段代码,判断一维数组传参时形参的写法是否正确?

#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};
 test(arr);
 test2(arr2);
}

答案是全都正确,前面几个相信大家已经掌握,主要来看一下最后一个int **arr为什么正确?

因为int *arr2[20]是一个指针数组,它里面存放的都是int*型的指针,我们说arr是数组首元素的地址,那就是指针的地址,而一级指针的地址可以用二级指针接收,所以形参写成int **arr是正确的。

总结一下就是:指针数组的传参,形参可以用二级指针接收。

4.2二维数组传参

二维数组传参,形参的部分可以是数组,也可以是指针:

void test3(char arr[3][5],int r,int c)
{}
void test4(char(*p)[5],int r,int c)
{}
int main()
{
  char arr[3][5] = { 0 };
  test3(arr, 3, 5);
  test4(arr, 3, 5);
  return 0;
}

因为数组名arr是一行的地址(即一维数组的地址),所以可以用数组指针作为形参接收。

以上就是二维数组传参,看下面一段代码,判断二维数组传参时形参的写法是否正确?

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

答案依次是:正确,错误(二维数组行可以省略,列不能省略),正确,错误,错误,正确,错误。(实参arr是一行的地址,只能用二维数组或者数组指针接收)。

4.3一级指针传参

很简单,一级指针传参就用一级指针接收就行。

#include <stdio.h>
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};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
}

现在来思考一个问题,我们已经写好了一个函数如下图,那要调用这个函数,都可以传给它什么参数呢?

void test(char* p)
{
  ;
}

这个能传的就多了,可以传给它变量的地址、数组名或者一级指针,

int main()
{
  char ch = 'w';
  char* ptr = &ch;
  char arr[10] = { 0 };
  test(&ch);//变量地址
  test(ptr);//一级指针
  test(arr);//数组名
}

4.4二级指针传参

同样的二级指针传参就用二级指针接收:

#include <stdio.h>
void test(int** ptr)
{
  printf("%d\n", **ptr);
}
int main()
{
  int n = 10;
  int* p = &n;
  int** pp = &p;
  test(pp);
  test(&p);
  return 0;
}

可见二级指针和一级指针的地址都可以用二级指针接收。

再次来思考一个问题,如果已经写好了一个函数如下图,那可以传给它什么参数呢?

void test(char** ptr)
{
  ;
}

除了上面已经说过的二级指针和一级指针的地址,指针数组的数组名也可以用二级指针接收。

int main()
{
  char ch = 'a';
  char* p = &ch;
  char* pp = &p;
  char* arr[10] = { 0 };
  test(pp);//二级指针
  test(&p);//一级指针的地址
  test(arr);//指针数组的数组名
}

5.函数指针

上文我们说过数组指针是指向数组的指针,类比可知函数指针就是指向函数的指针。

我们讲数组的时候说过,&数组名得到数组的地址,那如果我们要得到一个函数的地址呢?

同样的,&函数名即可,类似于数组名是数组首元素地址,函数名也是函数的地址。

那要将函数地址用一个变量存放就要用到函数指针了,函数指针的写法如下:

#include<stdio.h>
int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int (*pf)(int, int) = &Add;//pf是函数指针变量
  return 0;
}

(*pf)前后分别是函数返回类型和函数参数类型,函数指针的类型是int (*) (int,int),这类似于数组指针的写法。

下面再举个例子:

#include<stdio.h>
void test(char* pc, int arr[10])
{
  ;
}
int main()
{
  void (*pf)(char*, int[10]) = &test;
  return 0;
}

它的函数指针类型是 void (*) (char*,int [10])。

学了函数指针,我们就可以换一种调用函数的方式:

 

int Add(int x, int y)
{
  return x + y;
}
int main()
{
  //法一:
  int r = Add(3, 5);
  printf("%d ", r);
  //法二:
  int (*pf)(int, int) = &Add;//pf是函数指针变量
  int n = (*pf)(4, 5);
  printf("%d\n", n);
  return 0;
}

打印结果:8  9

我们说Add和&Add都是函数的地址,当写成Add的时候,调用函数可以直接写成 pf(4,5)。

学了函数指针,我们来看下面这两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

看起来很复杂,让我们来分析一下:

代码1:( *(void (*)() )0 ) ();

先看0,这里的0可以被看做一个数字,也可以被看做一个地址,当它被看做数字时是int型,被看做地址时是int*型的。代码中的void (*)()其实就是函数指针类型。

所以代码1的功能就是:调用0地址处的函数

第一步,先将0强制类型转换为void (*)()的函数指针。

第二步,调用0地址出的这个函数。(函数没有传参)。

代码2:void (*signal(int , void(*)(int)))(int);

代码2其实是signal函数的声明:

signal函数有两个参数,其中一个参数的类型是int,另一个参数的类型是函数指针类型void(*)(int),该函数指针指向的函数有一个参数,该参数的类型是int ,返回类型是void。

signal函数的返回类型也是void(*)(int)函数指针类型该函数指针指向的函数也是只有一个参数,该参数的类型是int ,返回类型是void。

代码2看上去十分复杂,其实我们可以对其进行简化,这里就不得不提一下曾经学过的类型重命名标识符typedef了:

typedef可以将类型重命名:

typedef unsigned int unit;
typedef int* ptr_t;
int main()
{
  unit u1;
  ptr_t p1;
  return 0;
}

使用时就可以直接用重命名后的unit 和 ptr_t。

同样的,也可以使用typedef将函数指针类型 void (*)(int)重命名为ptr_t,注意命名时要将ptr_t放在中间,即 typedef void (*ptr_t)(int)

那代码2就可以简化如下:

typedef void (*ptr_t)(int);
  ptr_t signal(int, ptr_t);

这就是typedef对代码的简化作用。

前面我们学了可以将整型指针放在数组中,可以将字符指针放在数组中,那函数指针能不能放在数组中呢?那就是函数指针数组?

这将在下节内容讲解,今天就学到这里,未完待续。。。

目录
相关文章
|
22小时前
|
存储 编译器 程序员
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针(下)
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针
4 0
|
1天前
|
C语言
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)(下)
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)
6 0
|
1天前
|
C语言
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)(上)
C语言进阶21收尾(编程练习)(atoi,strncpy,strncat,offsetof模拟实现+找单身狗+宏交换二进制奇偶位)
9 0
|
1天前
|
程序员 编译器 C语言
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
11 0
|
1天前
|
存储 程序员 编译器
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(上)
C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)
11 0
|
1天前
|
存储 C语言 C++
C语言进阶⑲(文件下篇)(文件读写+文本文件和二进制文件+EOF+文件缓冲区)(下)
C语言进阶⑲(文件下篇)(文件读写+文本文件和二进制文件+EOF+文件缓冲区)
9 0
|
1天前
|
存储 C语言 C++
C语言进阶⑲(文件下篇)(文件读写+文本文件和二进制文件+EOF+文件缓冲区)(上)
C语言进阶⑲(文件下篇)(文件读写+文本文件和二进制文件+EOF+文件缓冲区)
8 0
|
1天前
|
存储 编译器 C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(下)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
7 0
|
1天前
|
C语言
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose(中)
C语言进阶⑱(文件上篇)(动态通讯录写入文件)(文件指针+IO流+八个输入输出函数)fopen+fclose
8 0
|
1天前
|
C语言
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror(下)
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror
5 0