C语言复习笔记——指针(基于比特鹏哥)

目录

 前言

1. 指针是什么?

2. 指针和指针类型

 3. 野指针

4. 指针运算(结合例程理解)

5. 指针和数组

6. 二级指针

7. 指针数组

 8. 字符指针

9. 数组指针

9.1 数组指针的定义

9.2 &数组名VS数组名

9.3 数组指针的使用

10. 数组参数、指针参数

10.1 一维数组传参

10.2 一级指针传参

11. 函数指针

12. 回调函数 


 前言

1、一名合格的软件工程师,具备扎实的编程语言功底是最基本的要求,基础不牢,不仅会地动山摇,技术提升的空间也小。学习这条路,越往后面学,学到高级一点的知识,对基本功的要求也越来越高。所以一个程序员一定要注意夯实自己的基本功。

2、本人是嵌入式领域的一名学生,本次系列笔记的重点复习是指针、结构体、队列。因为早在寒假,我做一些公司的实用项目时,发现对这3个方面的知识都要求掌握得比较好,不然代码都看不懂,分析不清楚代码思路。

3、该系列笔记将会以提问+回答得方式来编写,基本上每个章节把问题都搞清楚以后,掌握得知识也就够用啦!

让我们一起学起来吧~

1. 指针是什么?

理解要点:
1、指针是内存中一个最小单元的编号,也就是地址。
2、平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
小结:指针就是地址,口语中说的指针通常指的是指针变量。

指针变量:我们可以通过&(取地址符)取出变量得地址,把地址放到另一个变量中去,这个变量就是指针变量

#include <stdio.h>
int main()
{
 int a = 10;//在内存中开辟一块空间
 int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
    //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
 return 0;
}

 总结:指针变量是用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

指针的大小在32位平台是4个字节,在64位平台是8个字节。

2. 指针和指针类型

我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。
char  *pc = NULL;
int   *pi = NULL;
short *ps = NULL;
long  *pl = NULL;
float *pf = NULL;
double *pd = NULL;
2.1 这里可以看到,指针的定义方式是: type + *
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
2.2 指针类型的意义是什么?
1、指针的类型决定了指针向前或者向后走一步有多大(距离)。
2、指针的类型决定了,对指针解引用(*p:取出此地址指向的变量)的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

 3. 野指针

3.1 野指针概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

3.2 野指针成因:

(1)变量未初始化

#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
 return 0;
}

(2)指针越界访问

#include <stdio.h>
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}

3.3 如何规避野指针

1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
#include <stdio.h>
int main()
{
    int *p = NULL;
    //....
    int a = 10;
    p = &a;
    if(p != NULL)
   {
        *p = 20;
   }
    return 0;
}

4. 指针运算(结合例程理解)

(1)指针+-整数

#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
     *vp++ = 0;
}

(2)指针-指针

int my_strlen(char *s)
{
       char *p = s;
       while(*p != '\0' )
              p++;
       return p-s;
}

(3)指针的关系运算

for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}
代码简化, 这将代码修改如下:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

5. 指针和数组

我们看一个例子:

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0;
}
运行结果: 数组名和数组首元素的地址是一样的。

结论:数组名表示的是数组首元素的地址

(2种情况除外:

1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数
组。
2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个数组就成为可能。
#include <stdio.h>
int main()
{
    int arr[] = {1,2,3,4,5,6,7,8,9,0};
    int *p = arr; //指针存放数组首元素的地址
    int sz = sizeof(arr)/sizeof(arr[0]);
    for(i=0; i<sz; i++)
   {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p+i);
   }
    return 0;
}
& arr [ i ]与 p + i,相等,是同一个地址。
所以 p+i 其实计算的是数组 arr 下标为 i 的地址。
那我们就可以直接通过指针来访问数组,如下:
int main()
{
 int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
 int *p = arr; //指针存放数组首元素的地址
 int sz = sizeof(arr) / sizeof(arr[0]);
 int i = 0;
 for (i = 0; i<sz; i++)
 {
 printf("%d ", *(p + i));
 }
 return 0;
}

6. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是: 二级指针
对于二级指针的运算有:
(1)*ppa 通过对 ppa 中的地址进行解引用,这样找到的是 pa *ppa 其实访问的就是 pa .
int b = 20;
*ppa = &b;//等价于 pa = &b;
(2) **ppa 先通过 *ppa 找到 pa , 然后对 pa 进行解引用操作: *pa ,那找到的是 a .
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

7. 指针数组

指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。让我们来对比学习:
int arr1[5];
char arr2[6];

那指针数组是怎样的?
int* arr3[5];//是什么?
arr3 是一个数组,有五个元素,每个元素是一个整形指针。
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

 8. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:
int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}
还有一种使用方式如下:
int main()
{
    const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}
代码 const char* pstr = "hello bit." ;
特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是 / 本质是把字符串 hello bit. 首字符‘h’的地址放到了 pstr 中。

9. 数组指针

9.1 数组指针的定义

数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint ; 能够指向整形数据的指针。
浮点型指针: float * pf ; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

9.2 &数组名VS数组名

我们知道 arr 是数组名,数组名表示数组首元素的地址。
&arr 数组名到底是啥?
我们看一段代码:
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}
可见数组名和 & 数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:
#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

根据上面的代码我们发现, &arr arr ,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是 数组的地址 ,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址 +1 ,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40(4*10).

9.3 数组指针的使用

那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码
#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0;
}
一个数组指针的使用:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
          printf("\n");
   }
}
void print_arr2(int (*arr)[5], int row, int col)
{
    int i = 0;
    for(i=0; i<row; i++)
   {
        for(j=0; j<col; j++)
       {
            printf("%d ", arr[i][j]);
       }
        printf("\n");
   }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
   print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行
    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    //可以数组指针来接收
    print_arr2(arr, 3, 5);
    return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];//是数组,整型数组,里面存放整型数据
int *parr1[10];//是数组,指针数组,用于存放10个整型数据的地址的数组
int (*parr2)[10];//是指针,数组指针,能够指向一个大小为10个整型数据的数组
int (*parr3[10])[5];//是指针,数组指针

10. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

10.1 一维数组传参

一维数组进行传参的时,函数参数可以写成数组或指针(因为数组名是地址,可用指针接收);

其中参数是数组时,数组大小可写也可不写;

参数是指针时要找到对应的指针类型才行;

(1)传递参数是普通数组  arr(3种方式):

#include<iostream>
using namespace std;
void test(int arr[]) {
 
}
void test(int arr[10]) {
 
}
void test(int* arr) {
 
}
int main() {
	int arr[10] = { 0 };
	test(arr);
	system("pause");
	return 0;
}

传递参数是指针数组  arr2(3种方式):

一级指针的地址可以用二级指针接收;对于数组 arr2,数组名是首元素地址,数组每个元素是int*,也就是说数组名是一个一级指针的地址,传入参数是数组名,那么就可用一个二级指针接收:int **arr

#include<iostream>
using namespace std;
void test(int* arr[20]) {
 
}
void test(int* arr[]) {
 
}
void test(int **arr) {
 
}
int main() {
	int* arr2[20] = { 0 };
	test(arr2); //数组名是首元素地址
	system("pause");
	return 0;
}

10.2 一级指针传参

#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;
}

11. 函数指针

首先看一段代码:

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

输出的是两个地址,这两个地址是 test 函数的地址。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:
void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
首先,能给存储地址,就要求 pfun1 或者 pfun2 是指针,那哪个是指针?
答案是:
pfun1 可以存放。 pfun1 先和 * 结合,说明 pfun1 是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。 故 void (*pfun1) ()是一个函数指针,可以存放函数的地址。
pfun2是函数 ,// 可以看出pfun2先和()结合,所以pfun2是一个 函数名 ,而其返回值为void *,故不是一个指针(不是函数指针),所以不能存放函数的地址。

12. 回调函数 

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。

首先演示一下qsort函数的使用: 

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}
使用回调函数,模拟实现 qsort (采用冒泡的方式)。
注意:这里第一次使用 void* 的指针,讲解 void* 的作用。
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

码字不易,希望喜欢的小伙伴别忘了点赞+收藏+关注,你们的肯定就是我创作的动力。

欢迎大家积极交流,本文未经允许谢绝转载!!!

猜你喜欢

转载自blog.csdn.net/weixin_62261692/article/details/130316937