C语言基础第17天:二级指针、main函数参数、常量指针和void指针的使用

指针

二级指针

**定义:**二级指针(多重指针)用于储存一级指针的地址,需要两次解引用才能访问原始数据。其他多重指针的用法类似,但实际开发中最常见的指针是二级指针。

cpp 复制代码
int a = 10; // a是普通变量,也就是原始数据
int *p = &a; // 一级指针,p指向a,解引用1次就可以获取a的值
printf("%d\n", *p); // 10
int **w = &p; // 二级指针,w指向p,解引用2次就可以获取a的值
printf("%d\n", **w);// 10
-----------------------------------------------------------------
int ***k = &w; // 三级指针,k指向w,解引用3次就可以获取a的值
printf("%d\n", ***k); // 10 int a1 = ***k; int *a2 = **k; int **a3 = *k; int ***a4 = k;

语法:

cpp 复制代码
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址

特点:
与指针数组的等效性二级指针与指针数组在某些时候存在等效性,但与二维数组不等效。二维数组名是数组指针类型,如 int (*)[3] ,而非二级指针。

cpp 复制代码
// 指针数组
int arr[] = {11,22,33};
int *arr_[] = {&arr[0],&arr[1],&arr[2]};
// 二级指针接收指针数组
char *str[3] = {"abc","aaa034","12a12"};
char **p = str; // p:数组首地址,行地址,默认0行 *p:列地址,默认0行0列    **p:列元素
cpp 复制代码
#include <stdio.h>
int main(int argc,char *argv[])
{
    // 字符串数组,字符类型的指针数组
    char *str[3] = {"abc","aaa034","12a12"};

    char **p = str;// str表示数组首地址,其实就是首元素地址 abc 这个字符串
    // p 存储的就是 abc的地址,指向的行,*p 访问到abc的列,默认首列
    // char str[] = "hello";
    // printf("%s\n", str);
    // char str2[20];
    // scanf("%s", str2); // str2就是一个地址
    // 打印字符串
    // for (int i = 0; i < 3; i++)
    // {
    //     printf("%s\n", *p);
    //     p++;
    // }
    // 打印字符
    int i = 0;
    while(**p != '\0')
    {
        printf("%-2c",**p);
        (*p)++;
    }
    printf("\n");
    return 0;
}

与二维数组的差异二维数组名是数组指针类型,直接赋值给二级指针会导致类型不匹配。

解引用:

字符型二级指针可直接遍历字符串数组,类似一维数组操作:

cpp 复制代码
#include<stdio.h>

void fun1();
void fun2();
void fun3();

int main()
{
	void fun1();
    void fun2();
    void fun3();
	
	return 0;
}

void fun1()
{
	char *arr[] = {"apple","hello","happy","oi"};
	int len = sizeof(arr) / sizeof(arr[0]);// int len = 5 * 8(指针) / 8(指针)  
	
	// printf("%d",len);
	for (int i = 0 ; i < len ; i++)
	{
		printf("%s\n",arr[i]);
	}
}

void fun2()
{
	char *arr[] = {"apple","hello","happy","oi"};
	int len = sizeof(arr) / sizeof(arr[0]);// 此时二级指针完全等价于指针数组 
	
	char **p = arr;// p 指向 arr 的首元素,也就是apple;
	
	for (int i = 0 ; i < len ; i++)
	{
		printf("%s\n", p[i]);		// 下标法 
		printf("%s\n", *(p + i));	// 指针法 
	}
}

void fun3()
{
	char *arr[] = {"apple","hello","happy","oi"};
	
	int len = sizeof(arr) / sizeof(arr[0]);
	char **p;
	int i = 0;
	
	// 遍历数组
	do
	{
		p = arr + i;// arr代表行 +i此时是行偏移,返回的地址 p 指向字符串;
		
		printf("%s\n", *p);// 对行地址解引用得到列地址
		
		// int a = 10; int *p = &a;
		// int a[] = {1,2,3};int *p = a; *p = {1,2,3};
		 
		i++;
	} while(i<len);
}

注意: 如果需要一个字符串类型的数组,我们可以选择使用二级指针或者指针数组,此时两者完全等价。

其他类型的二级指针需要两次解引用访问数据,常用于操作指针数组。

cpp 复制代码
int main()
{
    // 创建一个一维数组
    int arr1[] = {11,22,33,44,55,66}; // 11:0x11
    // 创建一个指针数组
    int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4],&arr1[5]}; 
    // [0]:0x22 --> 0x11

    // 用一个二级指针接收指针数组
    int **p = arr; // p 指向 arr,p存储的arr第一个元素的地址

    // 遍历数组
    for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
    {
        printf("%-6d", *p[i]); 
        // 下标法(1.指针偏移,2.对新指针解引用)

        printf("%-6d", **(p+i));
        // 指针法 p+i 元素地址偏移 元素地址,对元素地址解引用,返回元素值
    }
    printf("\n");
}

总结:

|--------------------|----------|----------|---------------------|
| 类型 | 本质 | 内存布局 | 等效性 |
| 二级指针(int**) | 指向指针的指针 | 指针的指针 | 与指针数组等效 |
| 指针数组(int*[]) | 元素为指针的数组 | 分散的指针 | 退化为二级指针 |
| 二维数组(int[][3]) | 数组的数组 | 连续的数据块 | 数组指针(int(*) [3]) |


main函数原型

**定义:**main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效。
main函数的完整写法:

cpp 复制代码
int main(int argc, char *argv[]){..}
int main(int argc, char **argv){..}

扩展写法:

cpp 复制代码
main(){} 等价 int main(){} // C11之后不再支持 缺省 返回类型
int main(void){} 等价 int main(){}
void main(void){} 等价 void main(){}
int main(int a){}
int main(int a, int b, int c){}
...

说明:

① argc,argv是形参,他们俩可以修改

② main函数的扩展写法有些编译器不支持,编译报警告

③ argc和argv的常规写法

argc:存储了参数的个数,默认是1个,也就是运行程序的名字

argv:存储了所有参数的字符串形式

④ main函数是系统通过函数指针的回调调用。

演示:

cpp 复制代码
#include <stdio.h>

int main(int argc, char **argv) // {"abc","aaa"} 对行地址解引用,得到首列地址
{
    // 访问参数个数 argc
    printf("argc=%d\n", argc);
    // 遍历参数(每一个参数都是一个字符串常量)
    for(int i=0;i< argc; i++)
    {
        printf("%s,%s\n", argv[i], *(argv+i));
    }
    printf("\n");
}

常量指针与指针常量

常量类型

① 字面量:直接使用固定值(如:12,hello,orange, 杨家辉三角),符号常量和枚举在编译器转换为了字面量

② 只读常量:用 const 修饰的变量,初始化之后不可修改。

cpp 复制代码
const int a = 10; // 只读常量
a = 21; // 编译报错

常量指针

  • 本质:指向常量数据的指针
  • 语法:
cpp 复制代码
const 数据类型 *变量名;
const 数据类型* 变量名;
  • 举例:
cpp 复制代码
const int *p; // p是常量指针
  • 特性:
    • 指向对象的数据不可改变( int a = 10; const int *p = &a; *p = 20; ,非法)
    • 指针本身的指向可以改变( int a = 10, b = 20; const int *p = &a; p = & b; ,合法)
  • 案例:
cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 10; // 变量
    const int *p = &a; // 常量指针
    // *p = 100; // 错误,指针指向的数据不可改变
    printf("%d\n", *p);// 10
    int b = 20; // 变量
    p = &b; // 正确,指针指向可以改变
    printf("%d\n", *p);// 20
}

指针常量

  • 本质:指针本身是常量,指向固定地址
  • 语法:
cpp 复制代码
数据类型* const 变量名;
数据类型 *const 变量名;
  • 特性:
    • 指向对象的数据可以改变( int a = 10; int* const p = &a; *p = 20; ,合法)
    • 指针本身的指向不可改变( int a = 10, b = 20; int* const p = &a; p = & b; ,非法)
  • 注意:

定义时必须初始化:

cpp 复制代码
int a = 10;
int* const p = &a; // 正确
  • 案例:
cpp 复制代码
#include <stdio.h>

int main()
{
    int a = 10; // 变量
    int* const p = &a; // 指针常量
    *p = 100; // 正确,指针指向的数据可以改变
    printf("%d\n", *p);// 100
    int b = 20; // 变量
    // p = &b; // 错误,指针指向不可改变
    printf("%d\n", *p);// 100
}

常量指针常量

  • 本质:指针指向和指向对象的数据都不可改变
  • 语法:
cpp 复制代码
const 数据类型* const 变量名;
const 数据类型 *const 变量名;
  • 举例:
cpp 复制代码
const int* const p; // p 是常量指针常量
  • 特性:
    • 指向对象的数据不可改变( int a = 10; int* const p = &a; *p = 20; ,非法)
    • 指针本身的指向不可改变( int a = 10, b = 20; int* const p = &a; p = & b; ,非法)
  • 注意:

定义时需要初始化:

cpp 复制代码
int a = 10;
const int *const p = &a; // 正确

关键点

  1. const 在 * 左侧:修饰数据(常量指针)

  2. const 在 * 右侧:修饰指针(指针常量)

  3. 函数参数优先使用常量指针,提高代码安全性

  4. 指针常量必须初始化,且不可重新指向


​​野指针、空指针、空悬指针

野指针

定义: 指向无效内存区域(比如未初始化、已释放或者越界访问)的指针称之为野指针。野指针会导致未定义(UB)行为。

危害:

  • 访问野指针可能引发段错误(Segmentation Fault)
  • 可能破坏关键内存数据,导致程序崩溃。

产生场景:

  1. 指针变量未初始化
cpp 复制代码
int *p; // p未初始化,是野指针
printf("%d\n", *p); // 危险操作:p就是野指针
  1. 指针指向已释放的内存
cpp 复制代码
int *p = malloc(sizeof(int)); // 在堆区申请1个int大小的内存空间,将该空间地址赋值给指针变量p
free(p); // 释放指针p指向的空间内存
printf("%d\n", *p); // 危险操作:p就是野指针
  1. 返回局部变量的地址
cpp 复制代码
int* fun(int a, int b)
{
    int sum = a + b; // sum就是一个局部变量
    return &sum; // 将局部变量的地址返回给主调函数
}

int main()
{
    int *p = fun(2,3);
    printf("%d\n", *p); // 危险操作:p就是野指针
}

如何避免野指针:

  1. 初始化指针为NULL

  2. 释放内存后立即置指针为NULL

  3. 避免返回局部变量的地址

  4. 使用前检查指针有效性(非空校验,边界检查)。

cpp 复制代码
int fun(int *pt)
{
    int *p = pt;
    // 校验指针
    if(p == NULL) // 结果为假 等价于 if(!p) 其实底层: if(p == 0)
    {
        printf("错误!");
        return -1;
    }
    printf("%d\n", *p);

    return 0;
}

空指针

**定义:**值为 NULL 的指针,指向地址 0x000000000000 (系统保留,不可访问)
**作用:**明确表示指针当前不指向有效内存,一般用作指针的初始化。

示例:

cpp 复制代码
int *p = NULL; // 初始化为空指针
free(p); // 释放后置空
p = NULL;

空悬指针

**定义:**指针指向的内存已经被释放,但未重新赋值。空悬指针是野指针的一种特例。

示例:

cpp 复制代码
char *p = malloc(100); // 在堆区分配100个char的空间给p
free(p); // 释放指针p指向的内存空间
printf("%p,%d\n", p, *p); // p可以正常输出,*p此时属于危险操作
// p指向的内存空间被回收,但是p指向空间的地址依然保留,此时这个指针被称作空悬指针

void与void*的区别

定义:

void**:**表示"无类型/空类型",用于函数返回类型或者参数。

cpp 复制代码
void func(void); // 没有返回值也没有参数,一般简写:void func();

**void*****:**通用指针类型(万能指针),可指向任意类型数据,但需要强制类型转换后才能解引用。

cpp 复制代码
void* ptr = malloc(4); // ptr指向4个字节大小的堆内存空间
// 存放int类型数据
int *p = (int*)ptr;
*p = 10;

// 存放float类型数据
float* p1 = (float*)ptr;
*p = 12.5f;

// 存放char类型数组
char* p2 = (char*)ptr;

// 以下写法完全错误
float* ptr = malloc(4);
int *p = (int*)ptr; // 此时编译报错,类型不兼容 float* int*

**注意:**只能是具体的类型( int*,double*,float*,char*... )和 void* 之间转换

注意事项:

  • void 不能直接解引用( *ptr 会报错 )
  • 函数返回 void* 需要外部接收的时候明确类型(不明确类型,就无法解引用)

示例:

cpp 复制代码
#include <stdio.h>
// 定义一个返回类型为void*类型的指针函数
void* proces_data(void* p)
{
    return p;
}

int main(int argc, char *argv[])
{
    // int类型
    int m = 10;
    int* p_int = &m;
    int* result_int = (int*)proces_data(p_int);
    printf("Integer value:%d\n", *result_int);

    // double类型
    double pi = 3.1415926;
    double* p_double = &pi;
    double* result_double = (double*)proces_data(p_double);
    printf("Double value:%lf\n", *result_double);

    return 0;
}