C指针使用笔记

常见函数及问题

calloc、malloc、realloc与free

malloc用于分配变量所需的内存空间,并返回一个指向该空间起始地址的指针。若申请内存空间失败,则返回NULL。malloc函数仅一个参数size,为要申请的内存大小,以字节为单位。malloc返回的指针类型为void*,因此实际使用时还需类型转换。

calloc与malloc返回值类型相同,开辟空间后还会将对应空间置0。有两个参数:待分配内存的元素个数以及每个元素的大小。因此,calloc常用于为数组开辟空间并清零。

对于已经分配到空间的内存,realloc可将该空间的大小重新调整。参数有两个:原内存空间对应的指针以及新内存空间的大小,返回一个新指针,指向重新分配的内存。

free无返回值,参数为待释放内存的指针变量。calloc、malloc、realloc函数申请到的内存可通过free释放。

#include <stdlib.h> 
char* p;
int* a;
p = (char*) malloc(4);  
//申请了一个char型指针,分配了4字节的内存空间
a = (int*) calloc(5, sizeof(int)); 
//申请了一个含5个元素的int数组并对数组清零。
p = (char*) realloc(p,10);
//将申请到的4字节内存扩大至10字节
free(p);               //使用完后释放掉p所占内存
free(a);
memset

函数原型:

memset(void *p, int c, size_t n)

从p指向的地址开始,对接下来的连续n个字节的内存空间,逐字节置为c。对于字符串,n为字符串长度乘以sizeof(char);对于数组,n为元素数量乘以每个元素的字节数。

对于int型数组,每个元素占4个字节,若c=1,逐个字节填充后数组中单个元素会被置为:

00000001 00000001 00000001 00000001

转成十进制是16843009。

因此,若要用memset初始化整型数组,仅0和-1可起到预期效果。

memcpy

函数原型:

void *memcpy(void *str1, const void *str2, size_t n)

将str2指向地址开始的连续n个字节的内存空间中的内容,复制到以str1指向的地址开始的连续n个字节的内存空间。返回str1。

memcmp

函数原型:

int memcmp(const void *str1, const void *str2, size_t n)

将str1指向的内存空间和str2指向的内存空间各自的前n个字节进行比较。返回值为 str1 - str2

整型数值在内存中以补码形式存储,而memcmp是直接比较内存数值,因此比较结果会出现负数比正数大的情况。

函数返回局部变量的地址

若在函数内部定义了变量且函数返回值为该变量的地址时,编译器一般会提示“function returns address of local variable”。这是因为离开函数后,函数内的局部变量会被注销,其地址对应的内存也被回收,可能会产生错误。

解决方法:

  • 定义局部变量时使用static修饰。
  • 使用malloc为局部变量分配内存并使用free释放。
  • 使用new为局部变量分配内存并使用delete释放。
指针运算优先级

最优先:

运算符说明结合性
[]数组下标左到右
()表达式或函数形参表左到右
. 或 ->结构体成员左到右

次优先:

运算符说明结合性
*解引用右到左
&取地址右到左
(类型)强制类型转换右到左
sizeof取类型大小右到左
野指针

作为局部变量的指针定义后未被初始化为NULL,则该指针为野指针,其会指向一块未知的内存空间,可能会导致程序运行错误。

因此,指针在定义后一定要初始化为NULL;指针指向的内容被free后也应将指针置为NULL,防止非法的内存访问。



二级指针的应用

保持在函数中对变量的更改

在使用函数处理链表等带有指针的复杂数据类型时,为了确保离开函数后对这些变量的更改依然有效,就需要使用二级指针。
看个例子:


void Test(int* q){
    cout<<&q<<endl;    
}
 
int main(){
    int a = 10;
    int* p;
    p = &a;
    cout<<&p<<endl;  
    Test(p);
    cout<<&p<<endl;        
    return 0;
}

在进入函数的时候,传入的参数是通过创建一个副本进行传入的。以上面的程序为例,在进入函数后系统另外创建了一个指针变量q,虽然q里面存的值与p一样,但q是一个新的变量,其地址与p是不一样的。

再看个例子:

void my_malloc(char **s){  
    *s=(char*)malloc(100);  
}
 
int main(){  
    char *p= NULL;  
    my_malloc(&p);
    //do something
    if(p)
        free(p);  
}  

如果不用二级指针,在函数中就是给一个副本指针s分配了空间,离开函数后副本指针s销毁,p从头到尾就没分配到空间,白忙活一场。

同理,对于作为参数传入函数的普通变量,若要令函数中对其修改在离开函数后仍生效,应使用一级指针将该变量的地址作为参数传入函数。

作为指针数组的形参

一维数组名是一种特殊的一级指针常量(下文会详细说明),指向的是数组的首元素。由于指针数组的元素均为指针,一维指针数组名指向的就是指针,因此其形参类型为二级指针。

实际上,若指针数组中的每个元素对应存储某个一维数组首元素的地址,那么可以通过二级指针变相实现二维数组的传参。


结构体指针

链表

结构体指针常用于构建链表,下面的程序展示了如何实现一个简单的单向链表。其中,head_id为头指针,其内部成员n存储链表的节点个数。current_id为遍历或插入节点过程中当前指向的节点。对于任一节点,其成员n为数据域,存储的是该节点的数据,p为指针域,指向该节点的前一个节点。

#include <stdio.h>
#include <stdlib.h>
 
struct student_info{
    int n;
    struct student_info *p;
};
 
typedef student_info id;
 
#define Allocate(x) x=(id*)malloc(sizeof(id))
 
 
int main(){
    id *head_id = NULL;
    id *current_id = NULL;
 
    Allocate(head_id);
    Allocate(current_id);
 
    head_id->p = NULL;
    head_id->n = 0;
    current_id->p = head_id;
 
    int num;
    while (scanf("%d", &num) != EOF){
        if (num == -1) break;
        head_id->n++;
        id * new_id = NULL;
        Allocate(new_id);
        new_id->n = num;
        new_id->p = current_id->p;
        current_id->p = new_id;
    }
 
    for (int i=0; i < head_id->n; i++){
        current_id = current_id->p;
        printf("%d ",current_id->n);
        //printf("%d ",current_id->p->n);
        //current_id = current_id->p;
    }
    return 0;
}

在结构体指针中访问其成员的两种方法:

(*指针变量名).成员名
 
指针变量名->成员名



一维数组名与一级指针

数组名是一种特殊的一级指针常量,它本身指向数组首元素且不可修改。此处将以下面的代码为例阐述数组名与普通指针的异同。

int a[3] = {1,2,3}; //与p一样,a的类型为int*
int* p = &a[0];
内存地址不同

系统不会分配内存空间给数组名本身,对数组名取地址还是得到数组名指向的地址。例如以下四项的地址是一样的:

a, p, &a, &a[0]

由于系统另外分配了内存存储指针p,因此&p与上述地址均不同。

sizeof不同

对数组名进行sizeof会得到整个数组的大小:

printf("%d\n", sizeof(a));  //输出12 (3*4)
printf("%d\n", sizeof(p));  //输出8(64位系统中,内存地址用8个字节(即64位)表示)
printf("%d\n", sizeof(&a)); //&a表示一个内存地址,输出也为8

因此,在使用含sizeof参数的函数(例如memset)要留意,对指向数组的指针进行sizeof无法得到实际的数组大小。

下标及运算不同

数组名和普通指针均可使用操作符“[]”以及“+”“-”符号(且加减相同的值移动的字节数也相同)。但数组名在符号使用上有一定的限制。

  • 对数组名不可取负数下标。a[-1]代表a[0]前一个地址的值。然而该值所处内存空间是未知的,如此取值甚至修改会带来不可预测的问题。而对于普通指针,若能保持其指向的内容不越界,是可以令下标为负数的:
p = &a[2];  
printf("%d\n", p[0]);   //等价于a[2]
printf("%d\n", p[-1]);  //等价于a[1]
  • 对数组名取下标后的运算与a的运算不同,a+1将在a的地址上向后偏移一个int(即4个字节)的距离,&a+1将偏离整个数组(即3*4=12个字节)的距离。
数组名退化为普通指针

在使用函数传递数组时,一般将数组名作为参数传入,由于参数一般声明为普通一级指针,而且系统会给参数开辟内存空间,因此数组名传入函数后会退化为普通指针。


二维数组名与数组指针

二维数组名是一个特殊的数组指针常量,以下面的二维数组为例,探讨二维数组名与数组指针的区别。

int a[2][3] = {1,2,3,4,5,6};  //与p一样,类型为int (*)[3]
int (*p)[3] = a;
内存分配不同

请添加图片描述

在二维数组中,a指向a[0],但a[0]、a[1]和一维数组的数组名性质和用法一致。系统对于a、a[0]、a[1]等均未分配内存空间,因此它们最终均指向a[0][0]。

另外,数组指针p中的p[0]、p[1] … 等与一维数组名的性质与用法一致,一般可将p[0]与a[0]等价,p[1]与a[1]等价……

以下10个地址都是&a[0][0](即0xfe80)。

&a, a, *a, a[0], &a[0], &a[0][0]
    p, *p, p[0], &p[0], &p[0][0]

由于系统给p分配了内存,&p与上述值都不同。

sizeof不同

对二维数组名sizeof得到了整个二维数组的大小。

printf("%d\n", sizeof(a));    //输出24(2*3*4)
printf("%d\n", sizeof(a[0])); //输出12(3*4)
printf("%d\n", sizeof(p));    //输出8(64位系统)
printf("%d\n", sizeof(&a));   //输出8

简单来说,数组指针除了有单独的内存地址,其他地方和二维数组名在使用上区别不大……

下标及运算不同

二维数组名和数组指针均可使用“[]”及“+”、“-”符号。下标不可为负以及“-”运算符对二维数组名的限制与一维数组名类似。

运算上,以下11个值都是&a[1][0](即0xfe8c,与前者差了3*4=12个字节的距离)。

a+1, *(a+1), a[1], &a[1], a[0]+3, 
p+1, *(p+1), p[1], &p[1], p[0]+3, &p[1][0]

特别的,&a+1将前进234=24个字节。

二维数组传参

二维数组名指向的是数组,而不是指针,因此不能将二级指针作为二维数组名的形参。另外,由于二维数组名a指向的a[0]无对应内存地址,本质上还是指向的是a[0][0]的地址,因此,虽然**a可以得到a[0][0],但:

int **b = a; 
printf("%p\n", b);  //输出的是a[0][0]的地址
printf("%d\n", *b); //输出的是a[0][0],也就是1
printf("%d\n", **b); //错误,对*b解引用,意味着将a[0][0](也就是1)作为内存地址,取其中的内容,也就是取内存地址0x1中存储的内容。

实际上,如果拿到了数组首个元素(即a[0][0]的地址)以及二维数组行、列的数量,通过对地址移动指定字节数便可访问到数组中的任意元素。因此通过普通的一级指针即可传递二维数组:

//函数定义
void PrintArr(int *p ,int n, int m){
    for (int i=0;i<n;i++)
        for (int j=0;j<m;j++) {
            int k = *(p + i*m + j);
            printf("%d ", k);
        }
}
//函数调用
PrintArr((int*)a, 2, 3);

C99标准支持变长数组,因此也可使用数组指针来传递,但数组大小要通过另一个形参事先声明:

//函数定义
void PrintArr(int n, int m, int (*p)[m]){     //参数表中,m要在(*p)[m]前声明
    for (int i=0;i<n; i++)
        for (int j=0;j<m; j++) 
            printf("%d ", p[i][j]);
}
//函数调用
PrintArr(n, m, a);

若非要使用二级指针在函数中传递二维数组,请参考上面二级指针作为指针数组形参的方法来实现。



void指针

性质

任意类型的指针均可直接赋值给void指针,但反过来void指针要通过强制类型转换来赋值给其他类型指针。

在ANSI C标准中不可对void指针类型进行加减等操作,但按GNU标准,void*与char*类型一致,每次++移动1字节的地址。

应用

由于任意类型均可直接赋值给void指针,函数中可以将参数或返回值设置为void指针类型以实现泛型编程。例如以下函数:

void Swap(void* a, void* b, int size) {//交换两变量的值
    char t[size];
    memcpy(t, b, size);
    memcpy(b, a, size);
    memcpy(a, t, size);
}

在GNU标准下,void指针可加减的特性为函数中数组的泛型提供了支持。以下面冒泡排序函数为例:

void Sort(void* a, int n, int size, int (*cmp)(void* t1, void* t2)) {
    for (int i=0; i<n; i++)
        for (int j=i+1; j<n; j++)
            if (cmp(a+i*size, a+j*size))
                Swap(a+i*size, a+j*size, size);
}

得知数组元素的大小,就能通过移动特定字节的地址精准定位数组中的每个元素。


参考资料

二级指针的作用详解_majianfei1023的专栏-CSDN博客

C 库函数 – malloc() | 菜鸟教程

结构体指针,C语言结构体指针详解

二级指针与二维数组

数组指针和指针数组的区别

C语言指针是C语言中非常重要的概念,它是程序设计中最基本的数据结构之一。指针可以用于在程序中访问和操作内存中的数据,它们是C语言中最强大、最灵活的工具之一。在本篇笔记中,我们将介绍C语言指针的基本概念和用法。 1. 指针的概念 指针是一个变量,它存储了一个内存地址。在C语言中,每个变量都有一个内存地址,指针可以用来指向这个地址。通过指针,我们可以访问和操作内存中的数据,这使得C语言非常灵活和强大。 2. 定义指针变量 在C语言中,定义指针变量有两个步骤。第一步是声明指针变量的类型,第二步是用"&"符号取得一个变量的地址,并将这个地址赋给指针变量。例如: ```c int num = 10; //定义一个整型变量num int *p; //声明一个指针变量p p = &num; //将num的地址赋给指针变量p ``` 这样,指针变量p就指向了变量num的地址。 3. 操作指针变量 有了指针变量之后,我们可以通过指针变量访问和操作内存中的数据。例如,要访问变量num的值,可以通过指针变量p来实现: ```c printf("num的值为:%d", *p); //输出num的值 ``` 在这个例子中,通过"*"符号来访问指针所指向的内存中存储的值。这个符号被称为“解引用符号”,它可以用来访问指针所指向的内存中存储的值。 4. 指针的运算 指针可以进行加、减运算,这种运算是基于指针的类型和指针所指向的内存的大小进行的。例如,如果定义了一个指向整型的指针变量p,那么p+1将指向下一个整型变量的地址。同样的,p-1将指向上一个整型变量的地址。 此外,指针还可以用来访问数组中的元素。例如: ```c int arr[5] = {1, 2, 3, 4, 5}; //定义一个整型数组 int *p; //声明一个指针变量 p = arr; //将数组的首地址赋给指针变量 printf("arr[0]的值为:%d", *p); //输出数组第一个元素的值 ``` 在这个例子中,将数组的首地址赋给了指针变量p,然后通过解引用符号来访问数组中的元素。 5. 指针的应用 指针是C语言中非常重要的概念,它可以用于很多方面。例如,可以通过指针来动态分配内存空间、访问硬件设备、实现复杂的数据结构等等。 总之,指针是C语言中非常重要的概念,程序员需要深入理解它的概念和用法,才能够在程序设计中发挥其强大的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值