目录
3. 指向常量的常量指针(const int *const ptr)
因为指针的内容比较多,也有点点稍微复杂,所以一篇也难以讲完,所以我打算分成几部分,不定时的更新,希望对你们有所帮助,而且考虑到一窍不通的,所以我打算循序渐进的方式讲解,望各位理解
一,基础概念
指针是C语言中一种特殊的变量,它存储的是内存地址,而不是数据值。这意味着指针指向了存储值的内存位置。通过指针,我们可以间接访问和操作这些值。
1,一级指针的故事
想象你在一个大图书馆里找一本特定的书。这本书的名字叫做“C语言的奥秘”。图书馆非常大,所以直接找这本书可能需要很长时间。幸运的是,你有一张纸,上面写着这本书的确切位置。这张纸上的信息就像是一个指针,它告诉你去哪里可以找到这本书。
2,创建指针
现在,让我们把这个故事转化为代码。首先,我们要有一本书(也就是一个变量):
int book = 1; // 这里的书就是一个整数变量,假设它代表了“C语言的奥秘”这本书。
接着,我们需要一张纸(一个指针),上面写着这本书的位置:
int *location = &book; // 这里的&book就是获取这本书的地址。
3,通过指针访问数据
你已经有了指向书的位置的纸条。如果你想知道这个位置具体是什么,你需要做的就是查看纸条:
printf("书的编号是:%d\n", *location); // 使用*来读取location指向的内容,也就是书的编号。
4,修改指针指向的数据
如果你想改变这本书的编号,你同样可以通过这张纸条来做:
*location = 2; // 这表示我们改变了书的编号。
printf("现在书的新编号是:%d\n", book); // 通过书本变量直接查看新编号。
二,const 修饰指针
1. 指向常量的指针(const int *ptr
)
这种指针不能用来修改其指向的值,但是指针本身可以改变,即可以指向别的地址。
想象你有一本非常喜欢的书,但这本书是图书馆的,所以你不能在书上做任何标记(即不能修改书的内容)。不过,你可以换一本书来阅读(即可以改变指针指向的地址)。
const int *ptr; // 指向常量的指针
这里的ptr
就像是你手中的这本图书馆的书,你不能修改它(书的内容是不可变的),但你可以随时换一本新书来阅读(指针可以指向另一个地址)。
#include <stdio.h>
int main() {
int value1 = 10;
int value2 = 20;
const int *ptr = &value1; // ptr 指向 value1
printf("ptr points to value: %d\n", *ptr);
// *ptr = 15; // 错误: 不能通过 *ptr 修改 value1 的值,因为 *ptr 是指向 const 的
ptr = &value2; // 正确: 可以改变 ptr 的指向
printf("Now ptr points to value: %d\n", *ptr);
return 0;
}
2. 常量指针(int *const ptr
)
这种指针的指向不能改变,但它指向的值可以通过指针被修改。
现在,假设你买了一本新书,并决定这将是你书架上的第一本书。你可以在书上随意做标记(即可以修改数据),但你不能把这本书换成另一本(即指针不能指向别处)。
int *const ptr = &book; // 常量指针
这里的ptr
就像是你书架上的第一本书,你可以在里面做标记(可以通过指针修改数据),但一旦放好,就不能再把它换成别的书了(指针不能改变指向)。
#include <stdio.h>
int main() {
int value = 10;
int *const ptr = &value; // ptr 是一个常量指针,指向 value
*ptr = 20; // 正确: 通过 ptr 修改 value 的值
printf("Value is now: %d\n", *ptr);
// ptr = &value2; // 错误: ptr 不能指向另一个地址
return 0;
}
3. 指向常量的常量指针(const int *const ptr
)
这种指针的指向不能改变,但它指向的值可以通过指针被修改。
最后,想象你从图书馆借来了一本珍贵的旧书作为研究。这本书既不能在上面做标记(即不能修改数据),你也不能把它换成另一本书(即指针不能指向别处),因为它是你研究的特定材料。
#include <stdio.h>
int main() {
int value = 10;
const int *const ptr = &value; // ptr 是一个常量指针,且指向一个 const 值
printf("Value is: %d\n", *ptr);
// *ptr = 20; // 错误: 不能通过 ptr 修改 value 的值
// ptr = &value2; // 错误: ptr 不能指向另一个地址
return 0;
}
总之const修饰谁,谁就不能通过该方式来去改变它的值。
三,指针的运算
指针运算允许我们在内存中进行导航。在C语言中,指针运算主要包括指针的加法、减法、比较等。
1. 指针的加法
指针的加法并不是传统意义上的数值相加,而是指针向后移动若干位置。例如,如果你有一个指向数组第一个元素的指针,通过加法,你可以让指针指向数组的第二个、第三个元素等。
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int *ptr = numbers; // 指向数组的第一个元素(数组名是数组首元素地址即number[0])
printf("First element: %d\n", *ptr);
ptr = ptr + 2; // 移动到第三个元素——30
printf("Third element: %d\n", *ptr);
return 0;
}
想象一排连续的停车位,你的车停在第一个位置上。指针加法就像是你向前走到第三个停车位,ptr + 2
意味着从当前位置向前移动两个停车位。
2. 指针的减法
指针的减法与加法相反,它使指针向前(向低地址方向)移动若干位置。例如,如果你有一个指向数组末尾元素的指针,通过减法,你可以让指针指向前面的元素。
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int *ptr = &numbers[4]; // 指向数组的最后一个元素
printf("Last element: %d\n", *ptr);
ptr = ptr - 3; // 移动到第二个元素——20
printf("Second element: %d\n", *ptr);
return 0;
}
如果你在一列排队的人群中站在最后,指针减法就像是你往队伍的前面移动几个位置,ptr - 3
意味着往回走三个人的位置。
3. 指针比较
指针比较是检查两个指针是否指向同一个地址,或者哪个指针指向的地址更高(或更低)。
#include <stdio.h>
int main() {
int var1 = 5, var2 = 10;
int *ptr1 = &var1, *ptr2 = &var2;
if (ptr1 == ptr2) {
printf("ptr1 and ptr2 point to the same location.\n");
} else {
printf("ptr1 and ptr2 point to different locations.\n");
}
return 0;
}
比较两个指针就像是比较两个人在一条直线上的位置。如果两人站在同一地点,则相等;如果一人比另一人更靠近起点,则他们的位置不同,可以相互比较谁更接近起点。
就上面的例子结果是ptr1 and ptr2 point to different locations.
我们换个例子来看:
#include <stdio.h>
int main() {
int var = 10;
int *ptr1 = &var; // ptr1 指向 var 的地址
int *ptr2 = &var; // ptr2 也指向 var 的地址
if (ptr1 == ptr2) {
printf("ptr1 and ptr2 point to the same location.\n");
} else {
printf("ptr1 and ptr2 point to different locations.\n");
}
return 0;
}
此时它的结果为 ptr1 and ptr2 point to the same location.
为什么两个相差不大的代码,却结果就不大相同?
这是因为当两个指针指向相同的内存地址时,这意味着它们都引用或访问同一个变量或数据结构的位置。在C语言中,变量的内存地址是唯一的,这个地址表示变量存储在内存中的具体位置。如果两个指针指向同一个地址,它们就能够访问和操作存储在那个地址的数据。
也就好比有一个房子,有钥匙自然是能够开门的,但是进入房子里面的途径不只是从大门进去,我还可以翻窗户,或者挖地道等,我想表达的意思是,不管采取什么方法,我唯一的目的就是进去,而在这里的代码中,不管谁获取我的地址,我不会因为不同的变量而改变我该值的地址
四,野指针
野指针是指向未知内存地址或已经释放的内存空间的指针。它们是危险的,因为试图访问或操作野指针指向的内存可能导致不可预测的行为,包括程序崩溃、数据损坏或安全漏洞。
野指针的产生
1.未初始化的指针:声明指针变量但未给它赋予明确的初始地址。
int *ptr; // 未初始化的指针,它的值是未定义的。
2.已释放的内存空间:指针指向的内存被释放(如通过free
函数)后,继续使用这个指针。
int *ptr = malloc(sizeof(int)); // 分配内存
free(ptr); // 释放内存
*ptr = 10; // 错误:尝试访问已释放的内存
这里稍微提一下malloc函数,避免有人不知道
malloc
是C语言中的一个标准库函数,用于在堆上动态分配内存。它的名字来自“memory allocation”的缩写,意味着内存分配。使用malloc
可以在程序执行期间分配指定大小的内存块。
基本用法
malloc
函数的原型定义在stdlib.h
头文件中,其基本用法如下:
void* malloc(size_t size);//size_t和unsigned int 基本一个意思
- 参数:
size
表示要分配的字节数。 - 返回值:成功时,
malloc
返回指向分配的内存块的指针。如果分配失败,返回NULL
。
也可以这样来理解:
可以将malloc
比作是去图书馆借阅空间来存放你的书籍。你告诉图书馆管理员(malloc
函数)你需要多少空间(size
参数)来存放你的书籍(数据)。如果图书馆有足够的空间,管理员会给你分配一个特定的书架位置(返回一个指针),你就可以在那里放置你的书籍了。如果图书馆没有足够的空间,管理员会告诉你没有空间可用(返回NULL
)
注意事项
- 使用
malloc
分配的内存块默认是未初始化的,其内容是不确定的。如果需要初始化为零,可以使用calloc
。 - 动态分配的内存必须显式释放,否则会导致内存泄漏。使用
free
函数来释放内存。 - 分配失败时
malloc
返回NULL
,因此在使用返回的指针之前应检查它是否为NULL
,以避免空指针引用的错误。
这里就稍微提一下,后面在详细的讲解吧!毕竟主要目的还不是这个
3.指针操作越界:指针超出了其应当访问的内存区域。
int arr[10];
int *ptr = arr + 20; // 越界的指针
那如何避免野指针的问题呢?
1.初始化指针:声明指针时,初始化为NULL
或一个已知的有效地址。
int *ptr = NULL; // 安全的做法
2.使用后置空:释放指针指向的内存后,立即将指针设置为NULL
。
free(ptr);
ptr = NULL; // 避免野指针
free是C语言中的一个标准库函数,用于释放之前通过malloc
、calloc
或realloc
函数动态分配的内存。释放内存是为了将不再使用的内存返回给系统,避免内存泄漏——一种程序不再能够访问的内存仍然被占用的情况。
free
函数的原型定义和malloc 一样都是在stdlib.h
头文件中,其基本用法如下:
void free(void* ptr);
- 参数:
ptr
是指向之前分配的内存块的指针。 - 返回值:
free
函数没有返回值。
可以将使用free
释放内存比作是归还图书馆的书籍。假设你从图书馆借了几本书(通过malloc
等函数动态分配内存),当你读完这些书后,你需要将它们归还给图书馆(通过free
释放内存),以便其他人也可以借阅。如果你不归还书籍(不释放内存),图书馆的书将会越来越少,其他人可能就没有书可借了(内存泄漏)。
3.谨慎操作指针:避免指针运算导致的越界访问。
五,assert断言
assert
是一个用于在C语言程序中进行断言的宏,定义在assert.h
头文件中。断言是一种调试技术,它允许程序员指定程序在特定点上应满足的条件。如果该条件为真(即,条件表达式的计算结果非零),程序继续执行;如果条件为假(即,条件表达式的计算结果为零),assert
宏会打印一条错误消息到标准错误输出,并终止程序执行。
基本用法
assert
宏的基本用法如下:
#include <assert.h>
assert(expression);
- 参数:
expression
是一个表达式。如果表达式的计算结果为假(0),则assert
会触发。
当assert
触发时,它会显示一条包含表达式、文件名和行号的错误消息。这有助于快速定位问题所在。
示例
让我们看一个简单的示例,使用assert
来检查一个函数参数是否符合预期的约束:
#include <assert.h>
#include <stdio.h>
// 计算整数数组的平均值
double calculateAverage(int *arr, int size) {
assert(size > 0); // 断言数组大小大于0
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return (double)sum / size;
}
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
double average = calculateAverage(numbers, size);
printf("Average: %.2f\n", average);
// 尝试传递大小为0的数组,将触发assert
average = calculateAverage(numbers, 0); // 这将导致程序终止
printf("This line will not be executed.\n");
return 0;
}
运行结果就是这样
当你运行这段代码时,它首先尝试计算一个整数数组的平均值,然后在尝试执行一个可能导致运行时错误的操作(即尝试计算一个大小为0的数组的平均值)时,通过assert
来防止该操作的执行。下面是对程序执行流程的详细解释:
第一部分:计算平均值
-
数组定义和初始化:
int numbers[] = {1, 2, 3, 4, 5};
定义并初始化一个包含5个整数的数组。int size = sizeof(numbers) / sizeof(numbers[0]);
计算数组的大小,即数组中元素的数量。这里size
被计算为5
,因为sizeof(numbers)
得到的是数组总字节大小,sizeof(numbers[0])
得到的是一个数组元素的字节大小。
-
计算并打印平均值:
calculateAverage(numbers, size);
调用calculateAverage
函数计算数组numbers
的平均值。传递给函数的参数是数组的指针和数组的大小size
。- 在
calculateAverage
函数内部,assert(size > 0);
确保传入的数组大小大于0。如果size
不大于0,则assert
触发,打印错误信息并终止程序。在这次调用中,size
为5,满足条件,不触发assert
。 - 函数接着计算数组的总和,然后除以元素数量(即
size
)来得到平均值,并返回这个值。 printf("Average: %.2f\n", average);
打印计算得到的平均值,格式化为保留两位小数。因此,输出是Average: 3.00
。
第二部分:触发assert
- 紧接着,程序试图再次调用
calculateAverage
函数,但这次使用0
作为数组的大小:average = calculateAverage(numbers, 0);
- 因为数组大小为
0
,不满足calculateAverage
函数中assert(size > 0);
的条件,assert
被触发。 - 当
assert
被触发时,它会打印一条包含出错的文件名、函数名、行号以及失败的条件表达式的错误信息,然后终止程序。因此,程序不会继续到printf("This line will not be executed.\n");
这一行,这条消息不会被打印,程序在assert
触发时就已经终止。
希望这样的解释你们能够理解assert断言,但是如果你不想断言并且认为代码没有任何问题
可以在其上方加入 NDEBUG
#define NDEBUG
#include<assert.h>