• C语言程序设计笔记(浙大翁恺版) 第九周:指针


    按照中国大学MOOC上浙江大学翁恺老师主讲的版本所作,B站上也有资源。原课程链接如下:

    https://www.icourse163.org/course/ZJU-9001

    由于是大三抽空回头整理的,所以可能前五章会记的内容比较简略。此外,作为选学内容的A0:ACLLib的基本图形函数和链表两章也没有做。西电的考试是机试,理论上学到结构体就能够应付考试了,但为了以后的学习考虑建议全学。

     

    其他各章节的链接如下:

    C语言程序设计笔记(浙大翁恺版) 第一周:程序设计与C语言

    C语言程序设计笔记(浙大翁恺版) 第二周:计算

    C语言程序设计笔记(浙大翁恺版) 第三周:判断

    C语言程序设计笔记(浙大翁恺版) 第四周:循环

    C语言程序设计笔记(浙大翁恺版) 第五周:循环控制

    C语言程序设计笔记(浙大翁恺版) 第六周:数据类型

    C语言程序设计笔记(浙大翁恺版) 第七章:函数

    C语言程序设计笔记(浙大翁恺版) 第八周:数组

    C语言程序设计笔记(浙大翁恺版) 第九周:指针

    C语言程序设计笔记(浙大翁恺版) 第十周:字符串

    C语言程序设计笔记(浙大翁恺版) 第十一周:结构类型

    C语言程序设计笔记(浙大翁恺版) 第十二周:程序结构

    C语言程序设计笔记(浙大翁恺版) 第十三周:文件

     

    指针

    指针

    取地址运算

    &运算符取得变量的地址

     

     

    运算符&

    scanf("%d", &i)里的&获得变量的地址,它的操作数必须是变量

    示例:

    #include 
    
    int main(void)
    {
        int i = 0;
        // printf("0x%x\n", &i);
        printf("%p\n", &i);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    0xbff62d6c
    
    • 1

     

    想让printf输出地址应该用%p,否则编译运行时可能会出现类型不匹配的警告

     

     

    示例2:

    #include 
    
    int main(void)
    {
        int i = 0;
        int p;
        p = (int)&i;
        printf("0x%x\n", p);
        printf("%p\n", &i);
        printf("%lu\n", sizeof(int));
        printf("%lu\n",sizeof(&i));
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    0xbff62d6c
    0xbff62d6c
    4
    4
    
    • 1
    • 2
    • 3
    • 4

     

    如果不做强制将取地址得到的结果转换成int,编译会出现类型转换警告

    在这里插入图片描述

     

    之前做编译时选择以32位架构编译,如果换成以64位架构编译,得

    0x5c961d28
    0x7fff5c961d28 
    4
    8
    
    • 1
    • 2
    • 3
    • 4

    可见&可以取出一个变量的地址,地址的大小和int是否相等取决于编译器,取决于是64位架构还是32位架构

     

     

    &不能取的地址

    必须是一个明确的变量才可以取地址,&不能对没有地址的东西取地址,如&(a+b)&(a++)&(++a)

     

     

    试试这些&

    • 变量的地址
    • 相邻的变量的地址
    • &的结果的sizeof
    • 数组的地址
    • 数组单元的地址
    • 相邻的数组单元的地址

    示例:

    #include 
    
    int main(void)
    {
        int i = 0;
        int p;
    
        printf("%p\n", &i);
        printf("%p\n", &p);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    0xbff62d6c
    0xbff62d68
    
    • 1
    • 2

    相邻整型变量ip的地址刚好差4,而32位架构下int的大小等于4B,说明这两个变量在内存中是紧挨着放的

    先定义的变量内存地址更高。这两个变量都是本地变量,它们被分配在内存的堆栈(stack)中,而在堆栈里面分配变量是自顶向下分配的

     

    示例2:

    #include 
    
    int main(void)
    {
        int a[10];
     
        printf("%p\n", &a);
        printf("%p\n", a);
        printf("%p\n", &a[0]);
        printf("%p\n", &a[1]);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    0xbff8dd44
    0xbff8dd44
    0xbff8dd44
    0xbff8dd48
    
    • 1
    • 2
    • 3
    • 4

    a的地址、直接拿a当作地址输出、a[0]的地址相等。相邻的数组单元之间的地址差距为4

     

    指针

    指针变量就是记录地址的变量

     

     

    指针就是保存地址的变量

    示例:

    int i;
    int* p = &i;
    
    • 1
    • 2

    *表示p是一个指针,指向一个int。把i的地址交给p

    p指向i意思是p里面的值是变量i的地址

     

    int* p,qint *p,q都表示p是一个指针,指向一个指针,而q是普通的int变量。换句话说我们是把*加给p而不是int*p是一个int,于是p是一个指针,而并不是说p的类型是int*

     

     

    指针变量

    指针变量的值是内存的地址

    普通变量的值是实际的值,指针变量的值是具有实际值的变量的地址

    在这里插入图片描述
     

     

    作为参数的指针

    void f(int *p) 在被调用的时候得到了某个变量的值

    int i = 0; f(&i); 在函数里面可以通过这个指针访问外面的这个i

    示例:

    #include 
    
    void f(int *p);
    
    int main(void)
    {
        int i = 6;
        printf("&i=%p\n", &i);
        f(&i);
        
        return 0;
    }
    
    void f(int *p)
    {
        printf(" p=%p\n", p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    &i=0xbff17d70
     p=0xbff17d70
    
    • 1
    • 2

     

     

    访问那个地址上的变量*

    *是一个单目运算符,用来访问指针的值所表示的地址上的变量

    可以做右值也可以做左值。如:int k = *p*p = k+1

    示例:

    #include 
    
    void f(int *p);
    void g(int k);
    
    int main(void)
    {
        int i = 6;
        printf("&i=%p\n", &i);
        f(&i);
        g(i);
        
        return 0;
    }
    
    void f(int *p)
    {
        printf(" p=%p\n", p);
        printf("*p=%d\n", *p);
        *p = 26;
    }
    
    void g(int k)
    {
        printf("k=%d\n", k);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    &i=0xbff17d70
     p=0xbff17d70
    *p=6
    k=26
    
    • 1
    • 2
    • 3
    • 4

     

     

    左值之所以叫左值,是因为出现在赋值号左边接收值的不是变量,而是值,是表达式计算的结果:a[0] = 2;*p = 3;,是特殊的值,所以叫左值

     

     

    为什么int i; scanf("%d",i);编译没有报错?

    正好是32位架构,整数和地址一样大,对scanf来说把一个整数还是地址传进去没啥区别,它认为传入的是某个地址。运行时因为scanf把读入的数字写入到不该写的地方而出错

     

    指针的使用

    指针应用场景一

    交换两个变量的值

    示例:

    #include 
    
    void swap(int *pa, int *pb);
    
    int main(void)
    {
        int a = 5;
        int b = 6;
        swap(&a, &b);
        printf("a=%d,b=%d", a,b);
        
        return 0;
    }
    
    void swap(int *pa, int *pb)
    {
        int t = *pa;
        *pa = *pb;
        *pb = t;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    a=6,b=5
    
    • 1

     

     

    指针应用场景二

    函数返回多个值,某些值就只能通过指针返回。传入的参数实际上是需要保存带回的结果的变量

    示例:

    #include 
    
    void minmax(int a[], int len, int *max, int *min);
    
    int main(void)
    {
        int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55,};
        int min,max;
        minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
        printf("min=%d,max=%d\n", min, max);
        
        return 0;
    }
    
    void minmax(int a[], int len, int *min, int *max)
    {
        int i;
        *min = *max = a[0];
        for ( i=1; i<len; i++ ) {
            if ( a[i] < *min ) {
                *min = a[i];
            }
            if ( a[i] > *max ) {
                *max = a[i];
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    min=1,max=55
    
    • 1

     

     

    指针应用场景二b

    函数返回运算的状态,结果通过指针返回

    常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:-1或0(在文件操作会看到大量的例子)

    但是当任何数值都是有效的可能结果时,就得分开返回了。后续的语言(C++,Java)采用了异常机制来解决这个问题

    示例:

    #include 
    
    /**
        @return 如果除法成功,返回1; 否则返回0
    */
    int divide(int a, int b, int *result);
    
    int main(void)
    {
        int a=5;
        int b=2;
        int c;
        if ( divide(a,b,&c) ) {
            printf("%d/%d=%d\n", a, b, c);
        }
        return 0;
    }
    
    int divide(int a, int b, int *result)
    {
        int ret = 1;
        if ( b == 0 ) ret = 0;
        else {
            *result = a/b;
        }
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

     

     

    指针最常见的错误

    定义了指针变量,还没有得到任何实际变量的地址之前,不能通过它去访问任何数据

     

    指针和数组

    为什么数组传进函数后的sizeof不对了?

     

     

    传入函数的数组成了什么?

    示例:

    #include 
    
    void minmax(int *a, int len, int *max, int *min);
    
    int main(void)
    {
        int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55,};
        int min,max;
        printf("main sizeof(a)=%lu\n", sizeof(a));
        printf("main a=%p\n",a);
        minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
        printf("a[0]=%d\n", a[0]);
        printf("min=%d,max=%d\n", min, max);
        
        int *p = &min;
        printf("*p=%d\n", *p);
        printf("p[0]=%d\n", p[0]);
        
        printf("*a=%d\n", *a);
        
        return 0;
    }
    
    void minmax(int *a, int len, int *min, int *max)
    {
        int i;
        printf("minmax sizeof(a)=%lu\n", sizeof(a));
        printf("minmax a=%p\n",a);
        a[0]=1000;
        *min = *max = a[0];
        for ( i=1; i<len; i++ ) {
            if ( a[i] < *min ) {
                *min = a[i];
            }
            if ( a[i] > *max ) {
                *max = a[i];
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    main sizeof(a)=68;
    main a=0xbff0fd10
    minmax sizeof(a)=4
    minmax a=0xbff0fd10
    a[0]=1000
    min=2,max=1000
    *p=2
    p[0]=2
    *a=1000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

     

    minmax里的a数组其实就是main里的a数组,在minmax里可以修改a数组

    p[0]表示认为p所指向的是一个数组,它的第一个单元

     

    如果将void minmax(int *a, int len, int *max, int *min);void minmax(int *a, int len, int *min, int *max){...}中的int *a改为int a[],编译运行后会警告对于函数参数里的数组其实是int *

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iZxkp3U-1659885923100)(C语言程序设计.assets/image-20220728145147746.png)]

    函数参数表中的int a[]样子像个数组,实际上是指针,满足sizeof(a)== sizeof(int *),但是可以用数组的运算符[]进行运算

     

     

    数组参数

    以下四种函数原型是等价的:

    • int sum(int *ar, int n);
    • int sum(int *, int);
    • int sum(int ar[], int n);
    • int sum(int [], int);

     

     

    数组变量是特殊的指针

    数组变量本身表达地址,所以

    • int a[10]; int *p=a; 直接用数组变量名,无需用&取地址
    • 但是数组的单元表达的是单个变量,需要用&取地址
    • a == &a[0]

     

    []运算符可以对数组做,也可以对指针做:

    • p[0] <==> a[0]

     

    *运算符可以对指针做,也可以对数组做:

    • *a=25;

     

    数组变量是const的指针,所以不能被赋值

    • int b[] = a;不行,数组变量之间不能做互相赋值,作为参数传递时实际做的是int *q = a;
    • int b[] <==> int * const b = ... const表示b是一个常数,创建后就不能再代表别的数组

     

    指针与const

    注:本节只适用于C99

     

    指针与const

    指针本身和所指的变量都可能是const

    在这里插入图片描述

     

     

    指针是const

    表示一旦得到了某个变量的地址,值不能再被改变,不能再指向其他变量

    int * const q = &i;     // 指针 q 是 const
    *q = 26;                // OK
    q++;                    // ERROR
    
    • 1
    • 2
    • 3

     

     

    所指是const

    表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const,那个变量可以被赋以别的值,指针也可以指向别的变量)

    const int *p = &i;
    *p = 26;               // ERROR!(*p) 是 const
    i=26;                  // OK
    p=&j;                  // OK
    
    • 1
    • 2
    • 3
    • 4

     

     

    这些是什么意思?

    int i;
    const int* p1 = &i;
    int const* p2 = &i;
    int *const p3 = &i;
    
    • 1
    • 2
    • 3
    • 4

    判断哪个被const了的标志是const*的前面还是后面,const*的前面表示所指是constconst*的后面表示指针是const

     

     

    转换

    总是可以把一个非const的值转换成const

    void f(const int *x);
    int a = 15;
    f(&a);                   // OK
    const int b = a;       
    
    f(&b);                   // OK
    b = a+1;                 // Error!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

     

    当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较小的字节数传递值给参数,又能避免函数对外面的变量的修改

     

     

    const数组

    const int a[] = {1,2,3,4,5,6,};

    数组变量已经是const的指针了,这里的const表面数组的每个单元都是const int

    所以必须通过初始化进行赋值

     

     

    保护数组值

    因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值

    为了保护数组不被函数破坏,可以设置参数为const。 如:int sum(const int a[], int length);

     

    指针运算

    指针运算

    给一个指针加1表示要让指针指向下一个变量

    如果指针不是指向一片连续的分配空间,如数组,则这种运算没有意义

    示例:

    #include 
    
    int main()
    {
        char ac[] = {0,1,2,3,4,5,6,7,8,9,};
        char *p = ac;
        char *p1 = &ac[5];
        printf("p  =%p\n", p);
        printf("p+1=%p\n", p+1);
        printf("*(p+1)=%d\n", *(p+1)); // *(p+n) <--> ac[n]
        printf("p1-p=%d\n", p1-p);
        
        int ai[] = {0,1,2,3,4,5,6,7,8,9,};
        int *q = ai;
        int *q1 = &ai[6];
        printf("q  =%p\n", q);
        printf("q+1=%p\n", q+1);
        printf("*(q+1)=%d\n", *(q+1));
        printf("q1-q=%d\n", q1-q);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    p  =0xbffbad5a
    p+1=0xbffbad5b
    *(p+1)=1
    p1-p=5
    q  =0xbffbad2c
    q+1=0xbffbad30
    *(q+1)=1
    q1-q=6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

     

     

    指针计算

    这些算术运算可以对指针做:

    • 给指针加、减一个整数(++=--=
    • 递增递减(++/--
    • 两个指针相减

     

     

    *p++

    取出p所指的那个数据来,完事之后顺便把p移到下一个位置去

    *的优先级虽然高,但是没有++

    常用于数组类的连续空间操作

    在某些CPU上,这可以直接被翻译成一条汇编指令

     

    示例:

    #include 
    
    int main(void)
    {
        char ac[] = {0,1,2,3,4,5,6,7,8,9,-1};
        char *p = &ac[0];
        
        // for ( p=ac; *p!=-1 ; ) {
        while ( *p != -1 ) {
            printf("%d\n",*p++);
        }
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

     

     

    指针比较

    • <<===>>=!=都可以对指针做
    • 比较它们在内存中的地址
    • 数组中的单元的地址肯定是线性递增的

     

     

    0地址

    现代操作系统都是多进程操作系统,它的基本管理单元是进程。操作系统会给进程分配一个虚拟的地址空间,所有的程序在运行时都以为自己具有从0开始的一片连续空间,32位机器大小为4GB,当然实际上用不了这么多

    所以任何一个程序里都有0地址,但是0地址通常是一个不能随便碰的地址,所以你的指针不应该具有0值

     

    因此可以用0地址来表示特殊的事情:

    • 返回的指针是无效的
    • 指针没有被真正初始化(先初始化为0)

     

    NULL是一个预先定义的符号,表示0地址。有的编译器不愿意你用0来表示0地址

     

     

    指针的类型

    无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。但是指向不同类型的指针是不能直接互相赋值的,这是为了避免用错指针

     

     

    指针的类型转换

    void*表示不知道指向什么东西的指针,计算时与char*相同(但不相通)

    这往往用在底层系统程序里,需要直接去访问某个内存地址所代表的一些外部设备、控制寄存器等等

    指针也可以转换类型。 如:int *p = &i; void *q = (void*)p;。这并没有改变p所指的变量的类型,而后人用不同的眼光通过p看它所指的变量

     

     

    用指针来做什么

    • 需要传入较大的数据用作参数
    • 传入数组后对数组做操作
    • 函数返回不止一个结果
    • 需要用函数来修改不止一个变量
    • 动态申请的内存…

     

    动态申请内存

    输入数据

    如果输入数据时,先告诉你个数,然后再输入,要记录每个数据。C99可以用变量做数组定义的大小,C99之前呢?

    int *a = (int*)malloc(n*sizeof(int)); 申请n*sizeof(int)个字节大小的内存。返回void *,需要类型转换为int *

     

     

    malloc

    在UNIX下man malloc

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MiQzIG5k-1659885923106)(C语言程序设计.assets/image-20220728234241457.png)]

    需要#include ,参数类型是size_t,可以暂时当作是int

    malloc申请的空间的大小是以字节为单位的

    返回的结果是void *,表示指针指向一块不知道是什么的内存,需要类型转换为自己需要的类型

     

    示例:

    #include 
    #include 
    
    int main(void)
    {
        int number;
        int* a;
        int i;
        printf("输入数量:");
        scanf("%d", &number);
        // int a[number];
        a = (int*)malloc(number*sizeof(int));
        for ( i=0; i<number; i++ ) {
            scanf("%d", &a[i]);
        }
        for ( i=number-1; i>=0; i-- ) {
            printf("%d ", a[i]);
        }
        free(a);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

     

     

    没空间了?

    如果申请失败则返回0,或者叫做NULL

    示例:

    你的系统能给你多大的空间?

    #include 
    #include 
    
    int main(void)
    {
        void *p;
        int cnt = 0;
        while ( (p=malloc(100*1024*1024)) ) {
            cnt++;
        }
        printf("分配了%d00MB的空间\n", cnt);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在32位架构下运行

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTl4Vg5H-1659885923108)(C语言程序设计.assets/image-20220729002757571.png)]

    在返回0之前输出信息告知分配空间失败,但是程序没有终止

     

     

    free()

    把申请得来的空间还给”系统“

    系统会记住申请的空间,申请过的空间,最终都应该要还,只能还申请来的空间的首地址

    free(NULL);没问题,0不可能是malloc得到的地址,free会判断如果参数是NULL就不做处理

     

     

    常见问题

    申请了没free —> 长时间运行内存逐渐下降

    小程序产生的内存垃圾没有问题,操作系统有相关机制保证程序运行结束时曾经使用过的所有内存都会被清除干净

    新手往往会忘了free,而老手往往找不到合适的free的时机

     

    free过了再free

    地址变过了,直接去free

  • 相关阅读:
    Java NIO模型(提供代码示例)
    Node.js的模块
    ps 科研图文字变清晰
    labview与stm32通信
    (详细图解过程) IDEA在创建类的的时候自动生成作者信息、时间等信息
    【MySQL从入门到精通】【高级篇】(二十一)数据库优化步骤_查看系统性能参数
    计算机毕业设计之java+ssm基于web的医药进出口交易系统
    摩尔信使MThings提供丰富的组态控件
    【Redis】Redis 淘汰、雪崩、击穿、穿透、预热
    安装MinGW-w64
  • 原文地址:https://blog.csdn.net/zimuzi2019/article/details/126219294