深入理解指针---入门篇

目录

一. const修饰指针

1.1 const修饰变量

1.2 const修饰指针

 二. assert断言

2.1 基本语法

2.2 工作原理

2.3 使用示例

2.4 重要特性

三.传值调用和传址调用

3.1 传值调用(Call by Value)

3.2 传址调用(Call by Reference)

3.3 对比表格

 四. 数组名的深入了解

4.1 数组名的基本特性

4.2 数组名的类型

4.3 数组名在表达式中的退化

4.4 数组名与指针的区别

4.5 多维数组名

4.6 数组名作为函数参数

4.7 特殊注意事项

五. 冒泡排序


前言: 在我们通过了指针初阶后 让我们继续进一步深入学习指针吧 下面是指针进阶一的内容

一. const修饰指针

首先什么是const呢?

const 是 C/C++ 中的一个关键字,用于声明常量(不可修改的变量)。它的主要作用是增强代码的安全性和可读性,防止意外修改不应该被修改的数据。

const 的基本用法:

1.1 const修饰变量

const int MAX = 100;  // MAX 是一个常量,不能修改

任何尝试修改 MAX 的操作都会导致编译错误:

MAX = 200;  // ❌ 错误:不能修改 const 变量

1.2 const修饰指针

 const 可以用于指针的不同位置,含义不同:

① const 在 * 左边(指向的数据不可变)

const int *ptr = &x;  // 指针指向的数据是常量,不能通过 ptr 修改

ptr 可以指向不同的变量,但不能通过 ptr 修改数据:

即*ptr不能改变  ptr可改变

② const 在 * 右边(指针本身不可变)

int *const ptr = &x;  // 指针本身是常量,不能修改指向

ptr 不能指向其他变量,但可以修改 *ptr

即ptr不能改变  *ptr可以改变

int x = 10, y = 20;
const int *ptr = &x;
*ptr = 30;  // ❌ 错误:不能修改 *ptr
ptr = &y;   // ✅ 可以修改指针指向

③ const 在 * 两边(指针和指向的数据都不可变)

const int *const ptr = &x;  // 指针和指向的数据都不能修改

既不能修改 ptr 的指向,也不能修改 *ptr

int x = 10, y = 20;
const int *const ptr = &x;
*ptr = 30;  // ❌ 错误:不能修改 *ptr
ptr = &y;   // ❌ 错误:不能修改指针本身

 二. assert断言

assert 是 C/C++ 中的一个宏,用于在程序中进行断言检查。它可以帮助开发者在调试阶段快速发现程序中的逻辑错误和非法假设。

2.1 基本语法

#include <assert.h>  // 需要包含头文件

void assert(int expression);

2.2 工作原理

  1. 当 expression 为真(非零)时,assert 不做任何操作
  2. 当 expression 为假(零)时:
    • 输出错误信息(包含文件名、行号和失败的表达式)
    • 调用 abort() 终止程序执行

2.3 使用示例

#include <stdio.h>
#include <assert.h>

int divide(int a, int b) {
    assert(b != 0);  // 断言除数不为0
    return a / b;
}

int main() {
    printf("10 / 2 = %d\n", divide(10, 2));
    printf("10 / 0 = %d\n", divide(10, 0));  // 这里会触发断言失败
    return 0;
}

运行结果如图

2.4 重要特性

  1. 调试辅助assert 主要用于调试阶段,帮助发现程序中的逻辑错误
  2. 可禁用性:可以通过定义 NDEBUG 宏来禁用所有断言
#define NDEBUG  // 放在包含assert.h之前
#include <assert.h>

可以看到程序不在报错 

assert( )的缺点是 引入了额外的检查 增加了程序的运行时间

一般我们可以在Debug版本中使用 在Release版本中选择禁用assert  在VS这种集成开发环境中 在Release版本中 直接就会被优化掉 这样在Debug版本有利于程序员排查问题 在Release版本不影响用户使用时程序的效率

在函数进阶篇 我们学习过Mystrlen函数的练习 学习了上面的内容之后 我们可以对它进一步优化

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include <assert.h>
int Mystrlen(const char* arr)
{
assert(arr!=NULL);
	if (*arr != '\0')
	return	1+Mystrlen(arr+1);
	else
    return 0;
}
int main()
{
	char arr1[10] = "abcdedg";
	int len = Mystrlen(arr1);
	printf("%d", len);
	return 0;
}

我们加入了assert(arr!=NULL)和const修饰char* arr 

避免了arr是野指针的情况和调用函数时误改了arr的情况

三.传值调用和传址调用

在编程中,函数参数的传递方式主要有两种:传值调用传址调用。它们在内存处理、性能影响和修改能力上有显著区别。

3.1 传值调用(Call by Value)

特点:

  • 创建副本:函数接收的是实参的副本,而不是原始变量
  • 不影响原值:函数内对参数的修改不会影响调用处的原始变量
  • 内存开销:需要额外的内存空间存储副本
  • 安全性高:不会意外修改原始数据

C语言示例:

#include <stdio.h>

void modify(int x) {
    x = x + 10;  // 只修改副本
    printf("函数内: %d\n", x);
}

int main() {
    int num = 5;
    modify(num);
    printf("主函数: %d\n", num);  // 原值不变
    return 0;
}

输出:

3.2 传址调用(Call by Reference)

特点:

  • 直接操作原值:函数接收的是实参的内存地址(引用)
  • 影响原值:函数内对参数的修改会直接影响原始变量
  • 内存高效:不需要创建副本
  • 可修改性:允许函数修改调用者的数据

C语言实现(通过指针模拟传址调用):

#include <stdio.h>

void modify(int *x) {  // 接收指针(地址)
    *x = *x + 10;      // 通过指针修改原值
    printf("函数内: %d\n", *x);
}

int main() {
    int num = 5;
    modify(&num);      // 传递地址
    printf("主函数: %d\n", num);  // 原值被修改
    return 0;
}

输出:

3.3 对比表格

特性传值调用传址调用
传递内容值的副本内存地址/引用
内存使用需要额外内存无额外内存消耗
修改原值能力不能
性能对于大对象效率低高效
安全性高(不会意外修改)低(可能意外修改)
典型语法func(int x)C: func(int *x) 

使用场景建议

使用传值调用:

  • 参数是基本数据类型(int, float等)
  • 不需要修改原始值
  • 需要确保函数不会意外修改数据

使用传址调用:

  • 参数是大对象(结构体、类等)
  • 需要修改原始值
  • 需要避免复制的性能开销
  • 实现多返回值(通过引用参数返回多个值)

特殊案例:const引用

C++中常用const引用来兼顾效率和安全性:

void printBigObject(const BigObject &obj) {
    // 可以读取但不能修改obj
    // 既避免了复制开销,又保证了安全性
}

总结

理解传值和传址调用的区别对编写高效、安全的代码至关重要。选择哪种方式取决于:

  1. 是否需要修改原始数据
  2. 参数的大小和复制成本
  3. 对安全性的要求

 四. 数组名的深入了解

4.1 数组名的基本特性

(1) 数组名代表数组首元素的地址

int arr[5] = {1, 2, 3, 4, 5};
printf("%p\n", arr);      // 输出数组首元素地址
printf("%p\n", &arr[0]);  // 同上,输出数组首元素地址

(2) 数组名不是指针

虽然数组名在很多情况下会退化为指针,但它本质上不是指针变量:

int arr[5];
int *p = arr;  // 合法,数组名退化为指针
arr = p;       // 非法!数组名不是左值,不能被赋值

4.2 数组名的类型

数组名的类型是"元素类型的数组",例如:

int arr[5];  // arr的类型是int[5]

sizeof运算符的行为

int arr[5];
printf("%zu\n", sizeof(arr));  // 输出20(假设int是4字节,5*4=20)

4.3 数组名在表达式中的退化

在大多数表达式中,数组名会退化为指向其首元素的指针:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 1;  // p指向arr[1]

例外情况(数组名不退化为指针):

  1. 作为sizeof的操作数
  2. 作为&的操作数
  3. 作为字符串字面量初始化字符数组

4.4 数组名与指针的区别

特性数组名指针变量
存储空间无独立存储空间有独立存储空间
赋值操作不可赋值可以赋值
sizeof结果整个数组大小指针本身大小
&操作结果整个数组的地址(类型不同)指针变量的地址
类型信息包含元素数量和类型只有指向类型信息

4.5 多维数组名

对于多维数组,数组名的行为类似但更复杂:

int matrix[3][4];  // 3行4列的二维数组
  • matrix的类型是int[3][4]
  • matrix退化为int (*)[4](指向包含4个int的数组的指针)
  • matrix[0]的类型是int[4],退化为int*

4.6 数组名作为函数参数

当数组名作为函数参数时,它总是退化为指针:

void func(int arr[]);   // 等价于void func(int *arr)
void func(int arr[5]);  // 同样等价于void func(int *arr)

即使指定了大小,编译器也会忽略,函数内部无法通过参数获知数组实际大小。

4.7 特殊注意事项

  1. 数组名不是左值:不能出现在赋值语句的左边
  2. 数组名取地址&arr的类型是指向整个数组的指针
    int arr[5];
    int (*p)[5] = &arr;  // p是指向包含5个int的数组的指针
    
  3. 字符串字面量:实际上是字符数组,行为类似数组名
    char *p = "hello";  // "hello"是char[6],退化为char*
    

五. 冒泡排序

在C语言中,排序算法是基础且重要的内容。以下是常见的排序算法及其实现,包括冒泡排序选择排序插入排序快速排序归并排序等,每种算法都有其特点和适用场景。

现在我们来学习冒泡排序 其他排序算法我将在不久后更新

原理:重复比较相邻元素,将较大的元素逐步“冒泡”到数组末尾。

例如数组顺序为 9 8 7 6 5 4 3 2 1 我们要将他们从小到大排序 那我们从第一个数字开始往后依次比较 将较大的元素逐步放到末尾 一趟冒泡排序的过程如下图

 一趟冒泡排序下来 将最大的数字9成功放在了数组末尾 那我们有9个数字 需要几次冒泡排序呢?

是的 最多需要8次 因为8个数字的位置确定了 最后一个数字自然也就是它应该所处的位置

具体实现代码以及相关注释如下

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
void MaoPao(int* arr, int n)//排序函数 参数是数组以及数组元素个数
{
    int num = 0;//用于交换的中间变量
    for (int i = 0; i < n - 1; i++)//冒泡排序的次数
    {
        for (int j = 0; j < n - 1 - i; j++)  // 优化内层循环范围
        {
            if (arr[j] > arr[j + 1])//相邻元素进行比较判断
            {
                num = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = num;
            }
        }
    }
}

int main()
{
    int n;
    printf("请输入你需要排序的数字个数\n");
    if (scanf("%d", &n) != 1 || n <= 0)  // 输入你要排序的数字个数 确保合法
    {
        printf("输入无效\n");
        return 1;
    }
    int arr[n]; 

    printf("请输入你需要排序的数字\n");
    for (int i = 0; i < n; i++)
    {
        if (scanf("%d", &arr[i]) != 1)  // 输入验证 确保输入合法
        {
            printf("输入无效\n");
            return 1;
        }
    }
    MaoPao(arr, n);//调用排序函数
    printf("排序结果如下\n");
    for (int i = 0; i < n; i++)//依次打印
    {
        printf("%d  ", arr[i]);
    }
    return 0;
}

以上就是本期所有内容 希望能对你有所帮助

基于gcc的stm32环境搭建源码+文档说明.zip,个人经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做毕业设计的学生和需要项目实战练习的学习者,可作为毕业设计、课程设计、期末大作业,代码资料完整,下载可用。 基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的stm32环境搭建源码+文档说明.zip基于gcc的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿方猛敲c嘎嘎

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值