数组与字符串

一. 相似性

  • 内存访问机制
    • 从内存的角度来看,数组是一块连续的内存空间,用于存储相同类型的数据元素(单变量多值)。例如声明int arr[5] = {1,2,3,4,5};时,这意味着在内存中分配了一块连续的空间,足以存储5个int类型的数据
    • 指针则是存储一个内存地址的变量。当将指针指向数组的名字(首元素),例如int *ptr = arr;实际上是将数组的首元素的内存地址存储在ptr中 本质是指针常量退化解引用
    • 无论是通过数组的下标操作符[] 还是指针的解引用操作符 * ,在底层都是通过地址偏移来访问内存。当你使用arr[2];时,编辑器会将其转化为*(arr+2),这里arr会被隐式地视为指向首元素的指针。对于指针prt,ptr[2]也会被转换为*(ptr+2)。在汇编层面,这种操作通常是通过将基地址(即arr或prt的地址)加上偏移量(这里是 2 * sizeof(int)) , 然后计算出的地址处读写数据
    • 这种地址偏移操作利用内存的线性特性,利用指针或数组都可以灵活地访问和 操作内存中的数据元素
  • 函数参数传递
    • 当数组作为函数参数传递时,会发生数组到指针的转化,这是因为在c/c++中,函数调用是通过栈来传递参数的,将数组完整地复制到栈上效率低且不灵活,所以数组会退化为指针。例如函数void func(int arr[]); 实际在调用时,传递的是数组arr的首元素地址,等同于void func(int *arr); 在函数内部,使用arr时,编辑器将其视为指针常量指针,相当于包含了所有数组元素的数组指针解一次引用即int(*ptr)[5]=&arr;后再*ptr,通过这个指针再解引用*ptr可以访问原数组元素,这使得函数可以处理不同大小的数组,但也带来了一个问题,即函数内部无法直接指导数组的实际大小,需要额外的参数来传递数组长度
    • 退化: int(*ptr)[5]=&arr;ptr=*ptr 等价于 int *ptr = arr 前者ptr指向包含所有元素数组的指针,所以*ptr指针是数组名,又因为数组名是指向首元素的地址, 便可指针法

二. 特殊关系

  • 数组名的本质
    • 数组名在大多数情况下被视为常量指针无法修改但退化后可解引用。它代表了数组在内存中的固定起始地址,不能被修改,退化后解引用后通过指针法 指针运算 线性访问
    • 但数组名与指针有本质区别。sizeof(arr)时,编辑器可以确定数组的大小,因为arr也代表整个数组。然而int *ptr = arr; sizeof(ptr), 计算的是指针本身的大小 除非int *ptr[5] = arr; 相当于数组本身传递给了数组指针。
    • 在编译期间,数组名的这种特性被用来确定数组的边界,防止越界访问,例如访问arr[5],编辑器会检查这个下标是否超出了数组的范围,因为它知道arr是大小为5的数组,属于受检异常编辑器可避免。编辑器无法轻易得知其指向的范围,非受检异常避免指针越界,因为指针可以指完数组之后可指向任意地址,可能出现严重的内存错误
  • 指针运算和数组边界
    • 指针可以进行算数运算,如ptr++ 底层是将指针的值增加sizeof(int)。对于数组名,退化前无法arr++ , 因为它是个常量指针
    • 指针元素在处理数组时很有用,如for(int *ptr = arr;ptr < arr+5;ptr++) 却可能访问为定义引发内存崩溃
    • 数组和指针在底层的这种关系,反映了c/c++的灵活性和对性能的追求。数组提供了方便的方式存储和访问连续的数据,而指针提供了对内存的灵活操作能力,同时这种灵活性也带来了风险,需要开发者理解内存布局和指针操作,避免野指针
  • 收敛优化
    • 数组和指针的特殊性质是由底层的内存访问和操作机制决定的
    • 在某些编辑器中,遍历简单数组,使用指针可能会比使用下标操作性能稍快
    • 多维数组int arr[3][4]其内存布局也是线性的,可利用指针将其视为一维数组*((int *)arr + i * 4 + j)

三. 字符串与数组

  • 存储形式
    • 字符串在c/c++中通常以字符数组的形式存储。例如 char str[]="Hello"; 定义了字符数组str,编辑器会其分配一块连续的内存空间,依次存储'H','e','l','l','o'以及字符串结束标识 \0。字符串本质就是特殊的字符数组
    • 由于字符串以数组形式存储,具有数组的特性,如通过str[2]下标就能访问字符元素,在编译期间,编辑器知道数组大小,即sizeof(str)会返回字符串数组占用的内存空间,包含\0, 上述就是大小6子节

四. 字符串与指针

  • 指针指向
    • 当定义char *ptr = str/"Hello" ,指针ptr指向字符串str的首字符地址。 也可通过解引用后指针法访问字符,因其底层也是通过基地址偏移实现字符串的访问
    • 对于字符串指针,通常不需要指定长度,而是通过判断字符串是否达到结束标识\0来确定是否遍历到字符串末尾,避免野指针

五. 字符串约定

  • 字符串约定,引用字符串本身时只需传递字符串起始地址(指针常量),便会自动读取到'\0'结束符号
    • 当字符串作为函数参数时,和普通数组一样,实际传递的是字符串的首字符地址,此时可指针法访问
  • char str[]="hello"; 等价于 char *str ="hello"; str都是指向首字符指针,便于字符串约定

六. 字符串和数组

  • 字符串本质就是特殊字符数组,特殊字符数组本质又是首元素地址(常量指针)
  • 也就是说,当字符串本身数组名被传递时,会退化为起始地址的指针(非数组指针),因为其本身代表起始地址的指针常量, 都可指针法, 前者存在字符串约定, 后者存在数组越界,都存在[]的隐式解引用
  • 常量指针,由于数组或字符串定义后它的起始地址就固定了,拥有常量的特性,故为常量指针,而仅有退化后才能进行解引用退化前/后都行 int *ptr=arr前退化 int (*ptr)[5]=&arr;ptr=*ptr后退化
  • 单变量存储多字符串.字符指针数组 char *ptr[2]={"nagisb","nagisb2"}; prinf(%s,ptr[0]); 每个元素存储对应首字符指针

七. 线性退化

  • 数组就是指针,仅仅是它们的表现方式不同,数组(默认名)是指针对象的成员属性 起始地址, 数组索引或字符串引用是指针直接操作内存的实例属性,即数组名/字符串直接拿来作为指针法,如行列式直接拿来作为方阵的值
  • 指针法的关键在于 起始地址指针步长 而数组名恰恰符合及其线性存储特性恰恰符合,体现了上层到底层的灵活性, 底层到上层的安全性
  • 普通数组(数组名)退化 *array 等价于 array[0] 数组指针(数组本身)退化 *array 等价于 array本身 再退化 *(*array+i) 直接退化
  • 数组传递 数组名传递: *array = array 直接可退化, 数组本身传递:*array=&array 间接可退化
  • 数组下标 array[0] 等价于 *array. 与 array[i] 等价于 *(array+i) 间接退化
  • 数组指针 , 引用时根据是前指向&temp整个数组解一次引用,或new直接分配已解一次引用,但 array[i] 不等价于*(array+i)
  • 下标运算[]语法糖支持数组 指针 字符串. 根据指针法指向注意步长 需要为起始地址指针 数组指针的步长为数组大小(本身) 只能*(*array+i)
  • 下标运算[]属于隐式退化,即下标运算[] 是 指针法*(array+i) 的充分条件,充分条件(前者能推出后者,反过来数组指针是特例)
//C:数组退化
#include <iostream>
using namespace std;
int main()
{
    int temp[5] = {1,2,3,4,5};
    for(int i=0; i<5; i++) {
        cout << *(temp+i) << endl; //直接退化
        cout << temp[i] << endl;   //间接退化
    }
}
//C:字符串退化
#include <iostream>
using namespace std;
int main()
{
    char str[] = "Hello,world";
    int a = sizeof(str)/sizeof(str[0]);
    for(int i=0; i<a; i++) {
        cout << *(str+i) << endl;   //直接退化
        cout << str[i] << endl;      //间接退化
    }
}
//C:数组指针退化
//1.指针无法利用隐式[]步长,2.嵌套内部数组退化
///: C:for遍历数组指针
#include <stdio.h>
int main() {
    int temp[5] = {1,2,3,4,5};
    // 先让*p2指向包含所有元素数组的指针
    int (*p2)[5] = &temp;
    for(int i=0; i<5; i++) {
        printf("%d\n", *(*p2+i));  //由于p2属于(数组本身),即 *p2 等价于 temp(数组名)
    }
    return 0;
}
//C:堆数组传递
#include <iostream>
using namespace std;

int main()
{
    // 动态分配,数组长度为 5
    int *array=new int [5]; //数组传递, 由于堆内存不确定,传递的是数组名而非数组本身
    for( int i=0; i<5; i++ )
    {
        array[i] = i;
        cout << array[i] << endl;
        cout << *(array+i) << endl;
    }
    //释放内存
    delete [] array;
}
posted @   NAGISB  阅读(5)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示