c语言对指针和数组的进一步理解(字符串,数组和指针进一步探究,const关键字,函数指针,几个例题)

字符串常量

  • 字符串常量在内存中的存储,实质是一个匿名数组
  • 匿名数组,同样满足数组两种含义的规定

数组两种含义回顾

虽然数组名本身不会改变其类型,但它在不同的情况下会被视为不同的类型。

在C语言程序中,数组的出现有两种可能的含义:

        1.代表整个数组
        2.代表其首元素的地址
当出现以下情形时,数组代表的是整个数组:

        1.在数组定义中
        2.在 sizeof 运算表达式中
        3.在取址符&中,此时&a便代表着整个一维数组的地址,虽然说整个数组的地址和首元素的地址看起来值是一样的,但是这里表示的是整个数组的地址,而不是首元素的地址,运算起来是不一样的。&a+1便跳过了整个数组,越界了,就变成了野指针。(因为这个指针指向的数据类型是数组类型)。
在这些情况下,数组可以被看作是一个整体的实体,类似于结构体。这种视角对于传递数组给函数、计算数组的长度、或者获取数组的地址都非常有用。


当出现其他情形时,数组都代表其首元素地址 。比如说在函数传递时传递的是数组名字,实际上就是传递了一个地址,也就是首元素的地址。a[1]=1;此时a也代表的是首元素地址。

对c语言字符串理解

  • 字符串常量在内存中的存储,实质是一个匿名数组
  • 匿名数组,同样满足数组两种涵义的规定

你可以把字符串常量如 "abcd" 看作是一个字符数组的名字来使用。虽然在底层它并没有一个显式的名字,但你可以像使用字符数组那样使用它。这意味着你可以通过其名字访问其元素,并且其名字可以被看作是该字符数组的首元素地址。这样看起来就和普通数组的使用没什么区别了

printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组
printf("%p\n", &"abcd");        // 此处 "abcd" 代表整个数组

printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址
char *p1 = "abcd";         // 此处 "abcd" 代表匿名数组的首元素地址
char *p2 = "abcd" + 1;     // 此处 "abcd" 代表匿名数组的首元素地址

"abcd"[1] <==> *("abcd" + 1)

char *p1 = "abcd";  

对于这里的p1,我们更好的理解应该是字符串首元素的地址,p1指向的是a,而不是"abcd",更没有指向b,这个指针根本不知道b的存在。

ok那你可能此时就会有一个疑惑,当我们此时进行操作:printf("%s", p1);我们会发现打印出来的不止有a,而是会将abcd全部打印出来。为什么呢?

因为printf 函数在打印字符串时,实际上并没有意识到字符串的具体内容或长度,它只是依据字符串起始地址开始读取字符,并持续输出,直到遇到字符串结束标志 '\0'(空字符)。这种行为确实有点像一个“瞎子”,它仅依据某个特定的终止条件来判断何时停止。

  • %d:打印整数(int 类型)。
  • %u:打印无符号整数(unsigned int 类型)。
  • %f:打印浮点数(floatdouble 类型)。
  • %c:打印单个字符(char 类型)。
  • %s:打印字符串(字符指针,char * 类型)。
  • %p:打印指针(void * 类型)。
  • %x:打印无符号整数的十六进制表示(unsigned int 类型)。

变长数组

  • 概念:定义时,使用变量作为元素个数的数组。
  • 要点:变长数组仅仅指元素个数在定义时是变量,而绝非指数组的长度可长可短。实际上,不管是普通数组还是所谓的变长数组,数组一旦定义完毕,其长度则不可改变。
  • 语法:变长数组不可初始化!!!即以下代码是错误的:
int len = 5;
int a[len] = {1,2,3,4,5}; // 数组 a 不可初始化

 因为变长数组的长度在运行时确定,而不是在编译时确定。

当你定义一个局部数组并且加上了初始化列表时,这个初始化列表是编译时确定的。虽然数组本身是在运行时分配内存的,但初始化值在编译时已经确定并被嵌入到程序的可执行文件中。

所以这就产生了一种矛盾,所以变长数组不可初始化!!!

c语言动态内存开辟相关函数

malloc函数用法

函数了解

推荐阅读下面文章,文章还提到了calloc函数使用方法,易于理解。

【C语言】malloc()函数详解(动态内存开辟函数)_malloc函数-CSDN博客

malloc()函数的功能是:向内存申请一块连续可用的空间,并返回指向块开头的指针.

void* malloc (size_t size);

函数的特点:

1.申请的空间没有初始化

2.malloc()函数在开辟的过程中遇到了无法分配请求的内存块(即遇到了开辟失败的情况),那么就会返回一个NULL指针,因此malloc的返回值一定要进行检查!

3.使用malloc()函数动态开辟的内存空间,使用完是必须使用free()函数释放还给操作系统的,如果不释放的话就会造成内存泄漏!

使用方法

在使用malloc时,一般参数传递的形式为(sizeof(要开辟的变量名)*要开辟的个数).

当然也可以直接给malloc传一个具体的数字作为参数,比如:malloc(40);这样malloc()函数就会开辟一个大小为40字节的空间给你使用.

对于基本数据类型通常直接写总数字就行了,但是对于结构体类型写sizeof(要开辟的变量名)*要开辟的个数,可以很合适地帮助我们申请到合适大小地空间。

简单使用示范

1. 动态分配一个整数的内存

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int)); // 分配一个 int 大小的内存块

    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1; // 退出程序
    }

    *ptr = 42; // 使用分配的内存
    printf("动态分配的整数值: %d\n", *ptr);

    free(ptr); // 释放分配的内存
    return 0;
}

2. 动态分配一个整数数组的内存

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 5;
    int *arr = (int *)malloc(n * sizeof(int)); // 分配 n 个 int 大小的内存块

    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1; // 退出程序
    }

    for (int i = 0; i < n; i++) {
        arr[i] = i * i; // 初始化数组元素
    }

    printf("动态分配的数组元素: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr); // 释放分配的内存
    return 0;
}

虽然说指针和数组有一些区别,但是大多数情况数组命就是指针,这里可以使用解引用来在内存中赋值,也可以直接用[] 。

const关键字

c语言中,const关键字主要在指针这块常用,其他地方很少使用。

  • const型指针有两种形式:①常指针 ②常目标指针
  1. 常指针:const修饰指针本身,表示指针变量本身无法修改。也就是不能改变指针的值,不能指向其他空间。


 

  1. 常目标指针:const修饰指针的目标,表示无法通过该指针修改其目标。

  • 常指针在实际应用中不常见。

  • 常目标指针在实际应用中广泛可见,用来限制指针的读写权限

例子:可以看到这里的strcpy函数,使用了常目标指针修饰了src,使得我们只能访问src指针指向的东西,但是不能改变这个指针指向的东西。

函数指针

下面这个例子,帮助我们复习了指针的定义,指针的赋值,指针的调用,并且这里和我们之前学的指针的结构和用法都是一样的。但是函数指针语法有一些不一样的地方,请往下看

#include <stdio.h>

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int (*p)(int a, int b) = &max; // 将max函数的地址赋给函数指针p
    int m = max(1, 2);            // 直接调用max函数
    printf("%d\n", m);             // 输出m的值
    int n = (*p)(1, 2);           // 通过函数指针p调用max函数
    printf("%d\n", n);             // 输出n的值
    return 0;
}
  • 概念:指向函数的指针,称为函数指针。
  • 特点:函数指针跟普通指针本质上并无区别,只是在取址和解引用时,取址符和星号均可省略

这样的话我们就只需要在定义指针的时候加个*,其他地方的*和&是不可以省略的,注意其他类型指针是不可以的,这只是函数指针的特殊语法。

刚才的代码就可以这样来写

#include <stdio.h>

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int (*p)(int a, int b) = max; // 将max函数的地址赋给函数指针p
    int m = max(1, 2);            // 直接调用max函数
    printf("%d\n", m);             // 输出m的值
    int n = p(1, 2);           // 通过函数指针p调用max函数
    printf("%d\n", n);             // 输出n的值
    return 0;
}

其实吧,函数名就是一个地址,所以这里才可以省略我们的&和*,当我们写了&和*,编译器在编译的时候也会自动将这些符号删除,编译器如此多此一举可能就是为我们初学者考虑吧。

练习题

1.

2.

3.

虽然地址本身没有类型,但通过指针的使用,我们实际上给地址赋予一种“上下文类型”,这有助于正确地处理和解释数据。比如说&a肯定是一个地址,你要说它没有类型的话,那么&a+1是不是就是简单地加一了呢?但实际上显然不是的,实际上还是跳过了整个数组。我们实际用到地址的时候也会考虑地址的类型。

所以当我们讨论编程中的地址时,通常需要考虑与之相关的类型。这是因为指针类型对于如何访问和解释内存中的数据至关重要。这里将地址赋值给指针的时候也有时候需要强制类型转换。使得左右两边类型相匹配。这里p1是一个long*类型指针,&a+1是一个数组指针。

图解:

地址中的运算通常是按字节为单位进行的,这意味着最小的地址单位是字节。这里我们将数据都写成了16进制,右边是我们的这个数组,每个元素对应左边占8个字节,并且我们这里就以小端序来画图。左边的每个小格子代表一个字节,因为为16进制形式输出,所以每个字节里面写有两个数字,这样更加清晰。将a这个地址转换为了long类型(不再适用指针中的运算规则),那么这里+1就是偏移一个字节(可不是偏移一位,最小的地址单元是字节)。所以最后以16进制形式输出时,输出的是:0x0200000000000010

4.测试当前平台的字节序

字节序(Byte Order)是指在计算机存储和传输数据时,字节(Byte)的排列顺序。

  1. 大端序(Big-Endian):(高位对应低地址)

    • 在大端序中,一个多字节值的最高位字节(即“大端”)存储在最低的内存地址处,其余字节按照大小递减的顺序存储。
    • 大端序在网络传输中较为常见,因为这是网络协议中数据表示的标准字节序。
  2. 小端序(Little-Endian):(低位对应低地址)

    • 在小端序中,一个多字节值的最低位字节(即“小端”)存储在最低的内存地址处,其余字节按照大小递增的顺序存储。
    • 小端序在x86架构的个人计算机上更为常见。

数据以大端序和小端序存储的时候可以看成一个萝卜存放在盒子里,不论萝卜是正着放置还是反着放置,都是那根萝卜,我们实际上在使用内存的时候并不需要来考虑字节的排列顺序。

下面这个程序可以帮助我们来测试我们电脑上面的字节序

这里给数据赋值的时候是以16进制来赋值的,而不是我们通用的十进制,因为这样更能让我们直观地看到每个字节里面存放的数据是什么,十进制的话计算机内部存储的是二进制,转换后就大变样了,还得我们在比较的时候算算对应字节存放的数据,麻烦。

这里我们将地址来转化为char *类型的地址,这样的话我们解引用的话只会解引用一个字节。

5.一个算法题

编写一个程序,求一个有N个元素的整型数组中子数组之和的最大值,子数组指的是一个数组中连续的若干个相邻的元素。子数组中元素个数至少为1个
例如:

int a[7] = {-2, 5, -1, 6, -4, -8, 6};

则子数组之和的最大值是 5+(-1)+6,即答案是 10。

这里显然不能用暴力三重循环,可以用下面的算法。

核心思想:用两个变量来保存max和sum,sum用来保存当前的累加和,这个累加和如果小于0,那么说明要么是一开始从头开始遇到的是负数,要么就是前面积攒的sum是非负数,忽然遇到一个超级大负数让sum<0,正不抵负。遇到第二种情况就只能说明我们的解肯定是在这个大负数的两边,不可能子数组跨过这个大负数。此时sum就清零,重新累加,而max则保存的是每次循环sum所有情况中的最大值。

所以代码如下:

当然这个代码显然没有完备性,对于一些特殊情况没有考虑到,比如全为负数,那这样就得不到正确答案,这个特殊情况大家可以自己写一下,我这里就不给出来了,这里在最前面的代码加上特殊情况的考虑就行了。

6.二维数组的传参

c语言中传递二维数组的时候是不是必须要将数组的行列信息传递,传递一维数组的时候也要必须将数组的大小信息传递,因为函数传递数组的时候都是传递了一个指针,缺少了数组的大小信息。当然字符数组是个特例,字符数组可以不用传,因为字符数组有个尾0,可以很容易知道数组长度。

传参时,我们必须要指定第二维的大小,二维数组我们应该像这里(x[m])这样来看,表明这个是一个数组,数组元素类型是int [n],这样不能少写n就相当于int类型不能写成in,这里的int [n]是数据类型,所以这也是而二维数组第二个[]中值不能省略的一个原因,这里x其实就是一个维数组,这个一维数组在传参的时候可以省略[]中的值,并且一维数组在传到函数中的时候,隐式转换为了一个指针,这里也可以写成*x。

二维数组中的x[0],是一维数组的名字,同时也代表一维数组中首元素的地址,

所以这里面x[0],x[1]都其实都是数组名字,而我们数组名的两个不同含义,当&a[0]的时候a[0]可以将一维数组看成一个实体,&a[0]的地址是一个指向整个一维数组的地址,等于我们的a,

所以二维数组的数组名有时候可以理解为数组的指针。sizeof的时候也一维数组看成一个实体。

同时a[0]也可以理解为一个指针,代表数组首元素地址,

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值