指针之旅:从新手到专家(1)

本文详细介绍了C语言中指针的基础概念,包括指针的创建、通过指针访问和修改数据,以及const修饰指针的不同类型。此外,还涵盖了指针的运算、野指针的产生与避免,以及使用assert断言进行错误检测。
摘要由CSDN通过智能技术生成

目录

一,基础概念

1,一级指针的故事

2,创建指针

3,通过指针访问数据

4,修改指针指向的数据

二,const 修饰指针

1. 指向常量的指针(const int *ptr)

 2. 常量指针(int *const ptr)

 3. 指向常量的常量指针(const int *const ptr)

三,指针的运算

1. 指针的加法

 2. 指针的减法

3. 指针比较

四,野指针

野指针的产生

基本用法

注意事项

五,assert断言

基本用法

示例

第一部分:计算平均值

第二部分:触发assert


因为指针的内容比较多,也有点点稍微复杂,所以一篇也难以讲完,所以我打算分成几部分,不定时的更新,希望对你们有所帮助,而且考虑到一窍不通的,所以我打算循序渐进的方式讲解,望各位理解

一,基础概念

指针是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语言中的一个标准库函数,用于释放之前通过malloccallocrealloc函数动态分配的内存。释放内存是为了将不再使用的内存返回给系统,避免内存泄漏——一种程序不再能够访问的内存仍然被占用的情况。

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来防止该操作的执行。下面是对程序执行流程的详细解释:

第一部分:计算平均值

  1. 数组定义和初始化

    • int numbers[] = {1, 2, 3, 4, 5}; 定义并初始化一个包含5个整数的数组。
    • int size = sizeof(numbers) / sizeof(numbers[0]); 计算数组的大小,即数组中元素的数量。这里size被计算为5,因为sizeof(numbers)得到的是数组总字节大小,sizeof(numbers[0])得到的是一个数组元素的字节大小。
  2. 计算并打印平均值

    • 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>

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

宿か命

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

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

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

打赏作者

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

抵扣说明:

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

余额充值