指针----------C语言经典题目(2)

指针基础

什么是指针,介绍一下你对指针的理解

指针是一种特殊的变量,存储的是内存地址,而不是普通数据的值。

比如int *p;表示p是一个指针,只能存储int类型数据的内存地址。

指针的类型(如int *、char *)决定了他能指向的数据类型,以及通过指针访问内存时的操作长度,比如int *指针每次移动会跨越4字节,对应int 的大小。

内存地址:通过&符号可以获取变量的地址。

复制代码
int a = 10;
int *p = &a;  // p 存储变量 a 的内存地址

可以通过解引用符号 *,来间接访问指针所指向的内存中的值:

复制代码
int a = 5, *p = &a;
printf("%d", *p);  // 输出 5(通过指针 p 访问 a 的值)
*p = 10;           // 通过指针修改 a 的值,此时 a 变为 10

指针的算术运算:指针可以进行加减整数操作,结果与指针类型相关:

复制代码
int a[3] = {1, 2, 3};
int *p = a;        // p 指向数组首元素(a[0])的地址
p++;                 // p 指向 a[1] 的地址(地址增加 sizeof(int), 4 字节)
printf("%d", *p);   // 输出 2

如何定义和初始化一个指针变量?

定义:

基本格式为:数据类型 *指针变量名

比如:

复制代码
int *p;        // 定义一个指向 int 类型的指针变量 
char *p;    // 定义一个指向 char 类型的指针变量 
float *p;    // 定义一个指向 float 类型的指针变量 

初始化指针变量:

需要为其赋一个合法的内存地址,或初始化为NULL空指针。

1.指向普通变量的地址:
复制代码
int n = 10;
int *p = &n;  // 定义时初始化,p 指向变量 n 的地址
2.指向数组元素的地址:
复制代码
int a[5] = {1, 2, 3, 4, 5};
int *p = a;    // 初始化指向数组首元素(等价于 &a[0])
int *p2 = &a[2];  // 初始化指向数组第 3 个元素(值为 3)
3.指向动态分配的内存(使用malloc)
复制代码
int *p = (int *)malloc(sizeof(int));  // 分配一个 int 类型的内存空间
*p = 100;  // 通过指针操作内存空间

什么是地址运算符(&)和解引用运算符(*)?

地址运算符&:

作用是获取一个变量的内存地址:

复制代码
int a = 10;
int *p = NULL; // 定义一个指向 int 类型的指针变量 p
p = &a; // 将变量 a 的地址赋给指针 p(&a 表示获取 a 的地址)

每个变量在内存中都有一个唯一的地址,&用于获取这个地址。

指针变量(p)的作用是存储其他变量的地址,而&是将变量的地址赋值给指针。

解引用运算符*:

作用是通过指针变量访问它所指向的内存地址中的值:

复制代码
int a = 10;
int *p = &a; // p 指向 a 的地址
printf("%d\n", *p); // 输出 10(*p 表示访问 p 指向的地址中的值,即变量 a 的值)
*p = 20; // 通过指针修改 a 的值,此时 a 的值变为 20

*用于操作指针所指向的数据,当指针指向一个变量时,*p等价于该变量本身。

*p = 20 等价于 a = 20。

空指针是什么?如何使用?

空指针是一个指针变量,其值为NULL。

1.初始化的时候使用:

复制代码
int *p = NULL; // 声明时初始化为空指针

是合法的指针值,这样的初始化可以避免成为野指针。

2.函数返回值的错误标识

使用malloc时,内存分配失败时返回NULL,可以用if语句来判断内存是否分配失败。

复制代码
int *a = (int*)malloc(sizeof(int));
if (arr == NULL) 
{
    printf("内存分配失败!\n");
}

3.数据结构的结束标志

在链表、树等数据结构中,空指针常用于表示节点的末尾(如链表的尾节点指针指向NULL)

复制代码
struct Node {
    int data;
    struct Node *next;
};

struct Node *head = NULL; // 空链表的头指针为 NULL

野指针是什么?如何避免?

野指针是指向非法内存地址或已释放内存空间的指针。

野指针的常见成因:

1.未初始化的指针。
2.指针指向的内存已释放:
复制代码
int *p = (int *)malloc(sizeof(int));
free(p); // 释放内存
*p = 10; // 此时 ptr 是野指针,解引用非法
3.指针越界访问:
复制代码
int a[5] = {1, 2, 3, 4, 5};
int *p = a;
p += 5; // 指向数组末尾之后的内存,解引用非法
4.函数返回局部变量的地址:
复制代码
int *fp() 
{
    int n = 10; // 局部变量,存储在栈上
    return &n; // 返回局部变量地址,函数返回后 num 的内存被释放
}
int *p = fp(); // ptr 是野指针

避免野指针:

1.初始化指针。

2.使用free()释放内存后,立即将指针置为NULL。

3.函数若需返回内存地址,应返回动态分配的内存(如malloc),或指向全局变量的指针。

复制代码
int *fp() 
{
    int *p = (int *)malloc(sizeof(int)); // 动态分配内存
    *p = 10;
    return p; // 合法,调用者需负责释放
}

指针运算

指针可以进行哪些运算?

指针的算术运算主要和指针所指向的数据类型有关,编译器会根据数据类型来计算实际的内存偏移量。

1.指针与整数的加减:

指针 + 整数:指针向前偏移 整数倍的sizeof(数据类型)个字节。

例如,若int *p指向地址0x1000,那么p + 1指向0x1004(int 占四个字节)

复制代码
int a[5] = {1, 2, 3, 4, 5};
int *p = a;           // p 指向 arr[0](地址 0x1000)
p = p + 2;              // p 指向 arr[2](地址 0x1008)

指针 - 整数:指针向后偏移整数倍的sizeof(数据类型)个字节。

2.指针与指针的相减:

当两个指针指向同一块连续内存区域(如数组元素)时,相减结果为他们之间的元素个数(而非字节差)。

复制代码
int *p1 = &a[4];      // 指向 arr[4]
int *p2 = &a[1];      // 指向 arr[1]
int diff = p1 - p2;     // diff = 3(4 - 1 = 3 个元素)

3.指针的自增和自减:

前置、后置自增:指针指向下一个元素。

复制代码
p++;                    // 等价于 p = p + 1,指向 arr[2]

前置、后置自减:指针指向前一个元素。

复制代码
p--;                    // 等价于 p = p - 1,指向 arr[1]

4.指针可以通过关系运算符(< <= > >= == !=)进行比较。

1.指向同一块连续内存区域时(比如数组元素),比较的是指针的地址顺序。
复制代码
int *p = a, *q = &a[3];
if (p < q)         // p 的地址小于 q 的地址,条件成立
{            
    printf("p在q的前面\n");
}
2.判断指针是否为空
复制代码
int *p = NULL;
if (p == NULL)        // 指针未初始化,避免野指针解引用
 {      
    printf("ptr is null\n");
}

5.指针的间接访问运算 *

通过解引用运算符*来获取指针指向的内存值,或修改值。

复制代码
int n = 10;
int *p = &n;
printf("%d\n", *p);     // 输出 10(访问 x 的值)
*p = 20;                 // 修改 x 的值为 20
printf("%d\n", n);       // 输出 20

6.指针的赋值运算

将一个指针的值(也就是地址),赋给另一个同类型的指针,使他们指向同一内存位置。

复制代码
int a[5] = {1, 2, 3, 4, 5};
int *p1 = a;           // p1 指向 a[0]
int *p2 = p1 + 2;        // p2 指向 a[2]
p1 = p2;                 // p1 现在也指向 a[2]

如何比较两个指针?

比较两个指针的地址值

== :判断两个指针是否指向同一个地址

!= :判断两个指针是否指向不同地址。

> < :当指针指向同一数组元素时,表示两个指针的相对位置。

指针与数组

数组名和指针有什么关系?

1.大部分表达式中,数组的数组名会被自动转换成指向数组首元素的指针。

2.当数组名作为函数参数进行传参时,被视为指针。

3.当用sizeof运算符来获取数组整体大小时,不会被看作指针,而是整个数组。

而如果sizeof指针的话,就是指针变量本身的字节大小。

4.用&来取数组地址时,数组名代表的是整个数组,而不是指针。

如何通过指针访问数组元素?

1.数组名等价于指向首元素a【0】的指针。可以直接解引用 ,用*(a + i)来访问。

*(a + i)完全等价于a【i】。

2.定义一个指针变量p,让它指向数组首元素,然后p++或p--来访问数组元素。

指针数组和数组指针的区别是什么?

指针数组:

本质是一个数组,数组的每个元素都是指针。例如:

int *a[5] 5个元素,每个元素都是int * 的指针。

因为[]的优先级要高于*,所以a会先与[]结合,表明是数组,每个元素都是指针。

数组指针:

本质是一个指针,该指针指向一个数组。例如:

int (*p)[5] p是一个指针,指向包含五个int 类型元素的数组。

()改变了优先级,使p先与*结合了,变成了指针,指向的目标是一个数组。

指针与函数参数

如何将指针作为函数参数?

1.当声明了一个函数,其参数为int类型的指针,

然后我们要在主函数调用它并修改调用者提供的变量时,就需要取地址来传递。

复制代码
// 函数声明:参数为 int 类型的指针
void fun(int *p) 
{
    *p = 100; // 通过指针修改指向的值
}

int main() 
{
    int n = 50;
    printf("修改前:%d\n", n); // 输出:50
    
    fun(&n); // 传递 n 的地址(&n)
    
    printf("修改后:%d\n", n); // 输出:100

    return 0;
}

2.当处理数组时,则传递数组名,因为数组的数组名就是指向数组首元素的指针。

可以用一个for循环来打印全部的数组元素。

复制代码
// 函数声明:参数为 int 类型数组的指针(等价于 int a[] 或 int *a)
void fun(int *a, int len) 
{
    int i = 0;    

    for (i = 0; i < len; i++) 
    {
        printf("%d ", *(a + i)); // 等价于 a[i]
    }
}

int main() 
{
    int a[] = {1, 2, 3, 4, 5};
    int len = sizeof(a) / sizeof(a[0]);
    
    fun(a, len); // 传递数组名(即首元素地址)
    
    return 0;
}

如何通过指针修改实参的值?

通过指针修改实参的值需要向函数传递实参的地址(指针),然后在函数内部通过解引用*指针来操作原始变量。

复制代码
// 定义函数:通过指针修改传入的整数值
void fun(int *p) 
{
    // 解引用指针并赋值,直接修改原始变量
    *p = 100;
}

int main() 
{
    int n = 10;
    printf("修改前:n = %d\n", n);    n  =  50

    // 传递 original 的地址(指针)给函数
    fun(&n);

    printf("修改后:n = %d\n", n);   n = 100

    return 0;
}

多级指针

什么是二级指针?请举例说明

二级指针是指向指针的指针,即该指针变量存储的是另一个指针变量的地址。一级指针指向普通变量,而二级指针则指向一级指针。

例如: int **pp 表示pp是一个二级指针,指向一个指向int类型的指针。

复制代码
int main() 
{
    int n = 10;          // 普通变量,存储整数 10
    int *p = &n;         // 一级指针 p,存储 num 的地址
    int **pp = &p;         // 二级指针 pp,存储一级指针 p 的地址

    // 访问变量的值
    printf("num = %d\n", num);                // 直接访问:10
    printf("*p = %d\n", *p);                  // 通过一级指针访问:10
    printf("**pp = %d\n", **pp);              // 通过二级指针访问:10

    // 访问指针的地址
    printf("&num = %p\n", &num);              // num 的地址
    printf("p = %p, &p = %p\n", p, &p);       // p 存储的是 num 的地址,&p 是 p 本身的地址
    printf("pp = %p, &pp = %p\n", pp, &pp);   // pp 存储的是 p 的地址,&pp 是 pp 本身的地址

    // 通过二级指针修改变量的值
    **pp = 20;
    printf("修改后 num = %d\n", num);         // 输出:20

    return 0;
}

二级指针可以用与动态内存分配中的指针传递:

复制代码
// 动态分配二维数组(行指针数组)
int r = 3, c = 4;

int **pp = (int **)malloc(r * sizeof(int *));

for (int i = 0; i < r; i++) 
{
    pp[i] = (int *)malloc(c * sizeof(int));
}

通过二级指针,每一行的长度可以不相同(如锯齿数组)。灵活性更高。

如何定义和使用二级指针?

定义:

定义二级指针时,要使用两个**,比如int **pp

使用:

可以两次解引用,来访问和修改一级指针所指向的变量的值。

在函数传参时,可以让函数修改一级指针的值:

复制代码
void fun(int **pp)
{
    static int n = 30;
    *pp = &n;
}

int main() 
{
    int n = 10;
    int *p = &num;  // 一级指针,指向 num
    int **pp = &p;  // 二级指针,指向一级指针 ptr

    printf("修改前 ptr 指向的值: %d\n", *p);
    fun(pp);
    printf("修改后 ptr 指向的值: %d\n", *p);

    return 0;
}

需要注意的是,在 fun 函数中,n 是一个局部变量,当函数执行完毕后,n 的内存会被释放。因此,在 main 函数中输出修改后 ptr 指向的值时,可能会得到一个不确定的值,这是因为 ptr 指向的内存已经被释放。因此需要在前面加上static。使用静态变量,确保其生命周期贯穿整个程序。

函数指针

什么是函数指针?如何定义和使用函数指针?

函数指针:

是一种指向函数的指针变量,它存储了函数在内存中的入口地址。

定义:

基本格式为:返回类型 (*指针变量名)(参数列表)

例如:int (*fun)(int a)

使用:

1.函数指针的赋值:

函数指针需要指向一个具体的函数,直接使用函数名即可获取函数的地址。:

复制代码
// 假设存在目标函数:int my_fun();
func_ptr = my_fun; // 或 &my_fun(取地址符可选,函数名本身会隐式转换为地址)

2.通过函数指针调用函数:

使用(*指针变量名)(参数):

复制代码
int  result = 0;

result = (*fun)(); // 调用 fun 指向的函数

如何通过函数指针调用函数?

复制代码
// 定义一个普通函数
int add(int a, int b) 
{
    return a + b;
}

int main() 
{
    int result = 0;

    // 声明函数指针,其指向的函数返回值为 int,参数为两个 int
    int (*fun)(int a, int b);
    
    // 将函数指针指向具体的函数(这里指向 add 函数)
    fun = add;
    
    // 通过函数指针调用函数
    result = fun(3, 5);
    printf("通过函数指针调用的结果:%d\n", result);
    
    return 0;
}

如何定义和使用指向函数的指针数组?

复制代码
// 定义三个不同的函数,用于后续赋值给函数指针
int add(int a, int b) 
{
    return a + b;
}

int sub(int a, int b) 
{
    return a - b;
}

int mul(int a, int b) 
{
    return a * b;
}

int main() 
{
    int x = 10, y = 5;

    // 定义指向函数的指针数组,每个元素指向返回int、参数为两个int的函数
    int (*fun[3])(int a, int b);

    // 将函数地址赋值给数组元素(函数名即为函数地址)
    fun[0] = add;
    fun[1] = sub;
    fun[2] = mul;

    // 通过函数指针数组调用函数
    printf("add: %d\n", func[0](x, y));       // 调用 add 函数
    printf("subtract: %d\n", func[1](x, y));    // 调用 sub 函数
    printf("multiply: %d\n", func[2](x, y));    // 调用 mul 函数

    return 0;
}

这种指针数组可以通过修改数组元素就切换不同的函数功能,提高代码拓展性。

回调函数

什么是回调函数?回调函数的应用场景有哪些?

定义:

简单来说,回调函数是"被其他函数调用的函数",当某段代码需要在特定事件发生时、或完成某个操作后调用一个自定义函数时,会将这个自定义函数的指针作为参数传递进去,供其在适当的时候调用。

回调函数本质是函数指针。

应用场景:
复制代码
// 回调函数:定义 int 类型数组的升序比较规则
int compare(const void* a, const void* b) 
{
    return (*(int*)a - *(int*)b);
}

int main() 
{
    int arr[] = {3, 1, 2};
    qsort(arr, 3, sizeof(int), compare); // 传递回调函数
    return 0;
}

比如C语言标准库里的qsort函数是一个通用排序函数,它不知道我要排序的数据是什么类型(整数、结构体、字符串等),也不知道我希望按什么规则去进行排序(升序、降序、自定义逻辑),因此。qsort函数需要我通过回调函数告诉它两件事:

1.排序顺序是什么?

2.数据类型的大小?

那么就可以写一个compare函数,这个就是传递给qsort的回调函数。

函数的参数选择const void* 。这是因为:

void * 是通用指针,可以接收任何数据类型(因为qsort是通用函数,可能有很多种不同的数据类型)

const表示不修改传入的数据,遵循只读的规则。

当qsort比较两个元素时,会把这两个元素的地址强制转换为const void*传递给compare。

然后在回调函数里写排序逻辑,比如:

复制代码
return (*(int*)a - *(int*)b);

表示当 a 的值(如 3)大于 b 的值(如 1)时,返回 3-1=2(正数),表示 a 应在 b 后面。

同样的,当 a 的值(如 1)小于 b 的值(如 2)时,返回 1-2=-1(负数),表示 a 应在 b 前面。

整个代码执行流的主动权在qsort手上,qsort来决定什么时候调用compare回调函数。

compare告诉qsort"怎么比",而qsort负责"怎么排"。

指针数组与数组指针

什么是指针数组?如何定义和使用指针数组?

定义:

指针数组是一个数组,其每一个元素都是指针。基本格式为:

数据类型 *数组名[数组长度] 比如int *a[3]

使用:

1.指向整数的指针数组:
复制代码
int n1 = 10, n2 = 20, n3 = 30;
int *p[3];  // 定义一个包含 3 个 int 类型指针的数组

// 初始化指针数组,使其指向不同的整数
p[0] = &n1;
p[1] = &n2;
p[2] = &n3;
2.指向字符的指针数组(常用与字符串):
复制代码
char *p[3] = 
{
    "Hello",   // 指针指向字符串常量 "Hello" 的首地址
    "World",   // 指针指向字符串常量 "World" 的首地址
    "C Language"
};
3.通过数组下标访问指针数组的元素,再通过解引用*来获取指向的值:
复制代码
// 示例 1 中访问整数的值
printf("Value of num1: %d\n", *p[0]);  // 输出 10

// 示例 2 中访问字符串
printf("First string: %s\n", p[0]);     // 输出 Hello

注意到:

示例1中,p[0]的类型是int *(指向int 的指针),需求是获取指针指向的整数的值(即n1的值10)。而解引用的作用,*p[0] 表示"访问 p[0] 所指向的内存地址的值",printf 中的%d 需要一个int 类型的参数,而p[0]是int * 类型,因此需要解引用。

示例2 中,p[0]的类型是char * (指向char 的指针),但这里它指向字符串"Hello"的首字符"H"的地址,而字符串本质上是字符数组,由char 类型的连续内存空间组成,以 "\0" 结尾。

printf的 %s 格式符要求传入一个char * 类型的参数(指向字符串首字符的指针),它会自动从该指针开始读取字符,直到遇到 '\0' ,

示例2的 p[0] 本身就是char *类型,满足%s 的参数要求,因此无需解引用。

如果加上解引用,则会得到首字符 'H' ,后续字符会读不到。

4.作为函数参数
复制代码
void fun(char *a[], int len) 
{
    int i = 0;

    for (i = 0; i < len; i++) 
    {
        printf("%s\n", a[i]);
    }
}

// 调用函数
fun(p, 3);
5.动态内存分配
复制代码
int n = 5;
int i = 0;
int *a[n];  // 定义指针数组

for (i = 0; i < n; i++) 
{
    a[i] = (int *)malloc(sizeof(int));  // 为每个指针分配内存
    *a[i] = i + 1;  // 赋值
}

// 访问数据
printf("Third value: %d\n", *a[2]);  // 输出 3

// 释放内存
for (i = 0; i < n; i++) 
{
    free(a[i]);
}

什么是数组指针?如何定义和使用数组指针?

定义:

数组指针是一个指向数组的指针变量,存储的是整个数组的起始地址。

数组指针的类型需要同时指定数组元素的类型和数组的长度,本质是指向一个特定长度数组的指针。

基本格式为: 数据类型 (*指针变量名)[数组长度]

例如:

复制代码
int a[5] = {1, 2, 3, 4, 5};        // 定义一个 int 类型的数组
int (*p)[5];                       // 定义数组指针,指向长度为 5 的 int 数组

初始化:

将数组指针指向同一类型、同长度的数组,直接使用数组名(数组名表示数组首地址):

复制代码
ptr = &a;  // 或直接写 ptr = arr;(因为数组名隐式转换为指向数组首元素的指针,但数组指针需要指向整个数组,此处 &a 更明确)

使用:

通过数组指针访问元素时,需要先解引用指针得到数组,在通过下标索引访问元素:

复制代码
printf("a[2] = %d\n", (*p)[2]);  // 等价于 a[2],输出 3

指针函数与函数指针

什么是指针函数?如何定义和使用指针函数?

定义:

指针函数是一种返回值为指针(即内存地址)的函数,本质是函数,只是返回的类型是指针。

使用:

1.返回字符串指针的函数:
复制代码
// 指针函数:返回一个指向 char 类型的指针(字符串)
char *fun() 
{
    // 动态分配内存(需手动释放)
    char *str = (char *)malloc(100 * sizeof(char));
    strcpy(str, "Hello, World!");
    return str; // 返回动态分配的内存地址
}

int main() 
{
    char *p = fun(); // 调用指针函数
    printf("%s\n", p);      // 输出字符串
    free(p);                 // 释放内存,避免内存泄漏
    return 0;
}
2.返回指向数组元素的指针:
复制代码
// 指针函数:返回指向 int 数组中最大值元素的指针
int *fun(int a[], int size) 
{
    int *max = &a[0]; // 初始假设第一个元素是最大值
    int i = 0;

    for (i = 1; i < size; i++) 
    {
        if (a[i] > *m) 
        {
            max = &a[i]; // 更新最大值指针
        }
    }
    return max; // 返回最大值元素的地址
}

int main() 
{
    int n[] = {10, 20, 30, 15, 25};
    int *max = fun(n, 5);
    printf("最大值:%d\n", *max); // 输出 30
    return 0;
}

指针函数和函数指针的区别是什么?

|-----|------------------|---------------------|
| 特征 | 指针函数 | 函数指针 |
| 核心 | 函数,返回指针 | 指针,指向函数 |
| 声明 | int *a() | int (*a)() |
| 用途 | 返回内存地址(动态分配内存) | 存储函数地址(回调函数) |
| 优先级 | 函数名优先级高于*,先调用函数 | ()优先级高于*,需括起来明确是指针 |

相关推荐
Walk Me Home4 分钟前
开源语音合成模型SparkTTS使用
开发语言·前端·javascript
o0向阳而生0o8 分钟前
23、.NET和C#有什么区别?
开发语言·c#·.net
XiaolongTu11 分钟前
从“啃算法”到“看见算法”:如何更好地学习算法和数据结构
算法·面试
想睡hhh12 分钟前
c++STL——list的使用和模拟实现
开发语言·c++·list
无名的小码农13 分钟前
【62期获取股票数据API接口】如何用Python、Java等五种主流语言实例演示获取股票行情API接口之沪深A股派现与募资对比数据及接口API说明文档
java·开发语言·python·股票api·股票数据接口·股票数据
RenderNow13 分钟前
深耕ffmpeg系列之AVPacket
linux
小鄒鄒13 分钟前
Java表达式1.0
开发语言·python
陈壮实的搬砖日记15 分钟前
一文看懂矩阵的秩和奇异值及python计算
深度学习·算法
BranH19 分钟前
Linux系统中的网络管理
linux·运维·服务器
夜羽rancho21 分钟前
二分查找,其实就这些了
前端·算法