目录
前言: 在我们通过了指针初阶后 让我们继续进一步深入学习指针吧 下面是指针进阶一的内容
一. 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 工作原理
- 当
expression
为真(非零)时,assert
不做任何操作 - 当
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 重要特性
- 调试辅助:
assert
主要用于调试阶段,帮助发现程序中的逻辑错误 - 可禁用性:可以通过定义
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
// 既避免了复制开销,又保证了安全性
}
总结
理解传值和传址调用的区别对编写高效、安全的代码至关重要。选择哪种方式取决于:
- 是否需要修改原始数据
- 参数的大小和复制成本
- 对安全性的要求
四. 数组名的深入了解
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]
例外情况(数组名不退化为指针):
- 作为
sizeof
的操作数 - 作为
&
的操作数 - 作为字符串字面量初始化字符数组
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 特殊注意事项
- 数组名不是左值:不能出现在赋值语句的左边
- 数组名取地址:
&arr
的类型是指向整个数组的指针int arr[5]; int (*p)[5] = &arr; // p是指向包含5个int的数组的指针
- 字符串字面量:实际上是字符数组,行为类似数组名
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;
}
以上就是本期所有内容 希望能对你有所帮助