C语言06--数组进阶

数组名含义

  • 数组名有两个含义:
    • 第一含义是:整个数组
    • 第二含义是:首元素的地址
  • 当出现以下情形时,那么数组名就代表整个数组:
    • 在数组定义中
    • 在 sizeof 运算表达式中 ,因此sizeof 计算的就是整个数组的大小。sizeof(arr)
      • sizeof(arr)为数组元素类型大小和数组元素个数的乘积
    • 在取址符&中 , 因此取得的地址是整个数组的地址。
  • 其他任何情形下,那么数组名就代表首元素的地址。即:此时数组名就是一个指向首元素的指针。
  • 示例:
cpp 复制代码
int a[3];                  // 此处,a 代表整个数组
printf("%d\n", sizeof(a)); // 此处,a 代表整个数组
printf("%p\n", &a);        // 此处,a 代表整个数组,此处为整个数组的地址

int *p = a;       // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
p = a + 1;        // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
function(a);      // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
scanf("%d",  a ); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]

C语言只有在第一含义的场合下表现为数组,其他大部分场合都表现为首元素的地址,当数组表现为首元素地址时,实际上就是一个指向其首元素的指针。数组运算实际上就是指针运算。

cpp 复制代码
#include <stdio.h>
 
int main()
{
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    printf("%d\n", sizeof(arr)); // 数组的整个大小 10 * 4 = 40
    printf("%d\n", sizeof(&arr[0])); // 数组首元素的地址,一般看pc的位数,32位的为4,64位的为8

    printf("%d\n", sizeof(&arr)); // &arr表示数组的地址,一般看pc的位数,32位的为4,64位的为8
    printf("arr = %p\n", arr); // 表示数组的首元素地址
    printf("&arr[0] = %p\n", &arr[0]); // 表示数组的首元素地址
    printf("&arr = %p\n", &arr); // 表示整个数组的地址
 
    printf("arr + 1 = %p\n", arr + 1); // 表示数组的的首元素地址+1,数组元素为int类型,所以这里为数组首元素地址+4
    printf("&arr[0] + 1= %p\n", &arr[0] + 1); // 表示数组的首元素地址+1,数组元素为int类型,所以这里为数组首元素地址+4
    printf("&arr + 1= %p\n", &arr + 1); //&arr代表整个数组的地址,+1表示加一整个数组大小,一整个数组大小为40字节
    printf("arr + 9 = %p\n", arr + 10);//arr数组首元素地址,+10个数组首元素地址大小,+40个字节

    return 0;
}
//打印结果:
// 40
// 8
// 8
// arr = 0xffffcbd0
// &arr[0] = 0xffffcbd0
// &arr = 0xffffcbd0
// arr + 1 = 0xffffcbd4
// &arr[0] + 1= 0xffffcbd4
// &arr + 1= 0xffffcbf8
// arr + 9 = 0xffffcbf8

数组下标

  • 数组下标实际上是编译系统的一种简写,其等价形式是:
cpp 复制代码
a[i] = 100;  等价于  *(a+i) = 100;
  • 根据加法交换律,以下的所有的语句均是等价的:
cpp 复制代码
  a[i] = 100;
*(a+i) = 100;
*(i+a) = 100;
  i[a] = 100;
  • 数组运算,等价于指针运算。

字符串常量

  • 字符串常量在内存中的存储,实质是一个匿名数组
  • 匿名数组,同样满足数组两种涵义的规定
  • 示例:
cpp 复制代码
printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组,打印5
printf("%p\n", &"abcd");        // 此处 "abcd" 代表整个数组,打印整个数组的地址

printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址,打印b
char *p1 = "abcd";         // 此处 "abcd" 代表匿名数组的首元素地址,P1指向数组的首元素
char *p2 = "abcd" + 1;     // 此处 "abcd" 代表匿名数组的首元素地址,P2指向数组第二个元素

注:字符串常量:

字符串常量是指在程序中直接使用双引号括起来的字符串。例如:"hello"、"world"等都是字符串常量。字符串常量在C语言中是不可变的,也就是说,一旦定义了字符串常量,就不能再修改它的值。字符串常量实际上是一个匿名数组,字符串即代表它的名字,如hello即代表"hello"的名字

在C语言中,你可以使用双引号括起来的任何字符序列来定义字符串常量。

在C语言中,字符串和字符串常量之间有一些重要的区别,我们可以通过以下几点来区分它们:

  1. 字符串常量是在双引号内的字符序列,例如"Hello"。字符串常量是在程序编译时就确定并存储在只读内存段中的。
  2. 字符串是字符数组,可以存储在堆内存或栈内存中。我们可以通过数组名或者指针来引用字符串。

字符串常量的本质表现是代表它的第一个字符的地址,这是因为在C语言中,字符串常量被存储为字符数组,并以空字符 '\0' 结尾。因此,当你声明一个字符串常量时,实际上是在内存中创建一个字符数组,并将该字符串中每个字符的地址存储在数组中。

在C语言中,字符串常量是不可变的,因此任何尝试修改字符串常量的操作都是不允许的。这也是为什么在前面的例子中,尝试使用指向字符串常量的指针来进行修改会导致崩溃的原因。

char *s = "hello",不能通过s改变"hello",而数组能够表示字符串并对字符串进行操作是通过申请一段字符存储空间,并复制字符串常量来表示字符串,而操作数组表示的字符串实际上是操作数组,所以char s[ ] = "hello"可以通过s来改变"hello"

数组表示字符串是把字符串常量直接复制一份过来,可访问可修改

指针是指向字符串的地址,因为是字符串常量,所以只可访问不可修改

当你写下 char str[] = "helloworld"; 时,实际上发生了两件事情:

  1. 在数据段中分配内存来存储字符串常量"hello"
  2. 创建了一个字符数组 str,并分配了足够的内存来存储字符串 "hello world"。
  3. 将字符串 "hello world" 中的每个字符复制到了字符数组 str 中,并在末尾添加了一个空

字符 '\0' 来表示字符串的结束。

  1. 编译器会自动在字符数组的末尾添加一个空字符 '\0' 来表示字符串的结束。这个过程是自动进行的,你不需要手动添加空字符

因此,你可以对字符数组 str 进行任意操作,因为它是可变的。这包括修改、反转、拼接等操作。但是,如果你通过指针直接对字符串常量"hello world" 进行操作,就会导致编译错误或者运行时错误

零长数组

  • 概念:长度为0的数组,比如 int data[0];
    • 数组是唯一一个允许合法越界操作的。
  • 用途:放在结构体的末尾,作为可变长度数据的入口(通过数组进行越界访问【合法】)
  • 示例:
cpp 复制代码
struct node
{
    /* 结构体的其他成员 */
    // 成员1
    // 成员2
    // ... ...
    
    int   len;
    char data[0];
};

// 给结构体额外分配 10 个字节的内存。
struct node *p = malloc(sizeof(struct node) + 10);
p->len = 10;    //结构体指针指向node,为node的len赋值为10

// 额外分配的内存可以通过 data 来使用
p->data[0] ~ p->data[9]

变长数组 (长度是一个变量)

  • 概念:定义时,使用变量作为元素个数的数组。
  • 要点:变长数组仅仅指元素个数在定义时是变量,而绝非指数组的长度可长可短。实际上,不管是普通数组还是所谓的变长数组,数组一旦定义完毕,其长度则不可改变。
  • 在实际编程中,有时数组的长度不能提前确定,如果这个变化范围小,那么使用常量表达式定义一个足够大的数组就可以,如果这个变化范围很大,就可能会浪费内存,这时就可以使用变长数组
  • 变量的值在编译期间并不能确定,只有等到程序运行后,根据计算结果才能知道它的值到底是什么,所以数组长度中一旦包含了变量,那么数组长度在编译期间就不能确定了,也就不能为数组分配内存了,只有等到程序运行后,得到了变量的值,确定了具体的长度,才能给数组分配内存,我们将这样的数组称为变长数组(VLA, Variable Length Array)。
  • 普通数组(固定长度的数组)是在编译期间分配内存的,而变长数组是在运行期间分配内存的
cpp 复制代码
int len = 5;
int a[len];  // 数组元素个数 len 是变量,因此数组 a 是变长数组

// 当数组被成功定义出来后,len的值发生任何改变都不影响数组的大小
len = 10 ;

int x = 2;
int y = 3;
int b[x][y]; // 数组元素个数 x、y 是变量,因此数组 b 是变长数组
int b[2][y]; // 数组元素个数 y 是变量,因此数组 b 是变长数组
int b[x][3]; // 数组元素个数 x 是变量,因此数组 b 是变长数组
  • 语法:变长数组不可初始化,即以下代码是错误的:
cpp 复制代码
int len = 5;
int a[len] = {1,2,3,4,5}; // 数组 a 不可初始化

先定义一个变量 n 和一个数组 arr,然后用 scanf() 读入 n 的值。有些初学者认为,scanf() 输入结束后,n 的值就确认下来了,数组 arr 的长度也随即确定了。这种想法看似合理,其实是蛮不讲理,毫无根据。

从你定义数组的那一刻起(也就是第二行代码),数组的长度就确定下来了,以后再也不会改变了;改变 n 的值并不会影响数组长度,它们之间没有任何"联动"关系。用 scanf() 读入 n 的值,影响的也仅仅是 n 本身,不会影响数组。

那么,上面代码中数组的长度究竟是什么呢?鬼知道!n 是未初始化的局部变量,它的值是未知的。

修改上面的代码,将数组的定义放到最后就没问题了:

在定义数组之前就输入 n 的值,这样输入的值才有用武之地。

结语

通过本篇博客,我们深入探讨了C语言中的数组,涵盖了其基本概念、初始化方式、访问和修改元素的方法,以及如何有效地使用数组进行高级编程。数组不仅是C语言中常用的数据结构,更是许多算法和程序实现的基础。

理解数组的运作机制及其在内存中的存储方式,不仅能够提升我们的编程技巧,还能帮助我们优化代码性能及资源管理,让我们的应用程序更为高效。无论是在处理数据集合,还是在实现复杂的算法时,掌握数组的各种进阶用法无疑会极大地拓宽我们的编程视野。

希望这篇文章能对您在学习和使用C语言数组方面有所帮助。数组的世界浩瀚无边,掌握了它们,您将能够驾驭更复杂的数据结构与算法。继续探索和练习吧,编程之旅才刚刚开始!

感谢您的阅读,如果您有任何疑问或想法,欢迎在评论区与我们分享。

相关推荐
VaporGas8 分钟前
掌握Java封装:以猜拳小游戏为例,深入理解OOP
java·开发语言·学习·面向对象编程·oop·猜拳游戏·封装思想
Bitup_bitwin13 分钟前
C++中的for-each循环
开发语言·c++
martian66514 分钟前
学懂C++(五十四):掌握 C++11 标准:提升开发效率与安全性的关键
开发语言·c++
小tenten36 分钟前
js延迟for内部循环方法
开发语言·前端·javascript
CJH~44 分钟前
Java入门:09.Java中三大特性(封装、继承、多态)01
java·开发语言·单例模式
我的运维人生1 小时前
JavaScript在网页设计中的应用案例
开发语言·javascript·ecmascript·运维开发·技术共享
计算机学姐1 小时前
基于Python的可视化在线学习系统
开发语言·vue.js·后端·python·学习·mysql·django
计算机学姐1 小时前
基于Python的电影票房数据分析系统
开发语言·vue.js·hive·后端·python·spark·django
qincjun1 小时前
数据库第一章:库的操作
c语言·数据库·c++
jianglq1 小时前
C++20 新特征:Ranges库初探
开发语言·c++20·c++20新特征