目录

数组和指针


数组和指针

1 数组的概述

1.1 声明数组

数组由 数据类型 相同的一系列元素组成。

需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。

C 语言中,普通数组的维度是固定的,必须是常量,不能是变量; VLA(C99),可变数组可以使用变量声明数组;后续说

普通变量可以使用的类型,数组元素都可以使用。

数组声明示例:

int main(void)
{
    float candy[365]; /*内含365个float类型元素的数组*/
    char code[12]; /*内含12个char类型元素的数组*/
    int states[50]; /*内含50个int类型元素的数组*/
}

下面的数组声明是等价的,声明 arr4 是一个内含 4 个 int 的数组

int arr4[4]; // C语言语法
int[4] arr4; // 其它语言支持的语法

只要有 方括号([]) 就都是数组,方括号中的数字表明数组中的元素个数

如果要 访问 数组中的元素,通过使用数组 下标数字(也称为索引)表示数组中的各元素。

数组的元素的编号从 0 开始。

1.1.1 数组和数组元素是两个概念

数组可以看做是一个"容器",容器本身的值是指针(数组的第一个元素的地址)

数组容器中的元素,就是每一个具体的"object",类型是固定的

“object” : int,float,string,boolean,struct,array,object…

1.2 初始化数组

数组通常被用来储存程序需要的数据。

只储存单个值的变量又叫做 标量变量(scalar variable)

用以 逗号 分隔的 元素值列表(用花括号括起来)来初始化数组,各元素值之间用 逗号 分隔。 在逗号和元素值之间可以使用空格。

int num[4] = {1,2,3,4};

1.3 给数组元素赋值

声明数组之后,就可以借助数组 下标(或索引)给数组元素赋值。

注意点:在 C 语言中,不允许把数组作为一个单位赋值给另一个数组,除了初始化以外也不允许使用花括号列表的形式赋值。

案例代码:

# define SIZE 5
int main(void)
{
    int oxen[SIZE] = {5,3,2,8}; // 初始化没问题
    int yaks[SIZE];

    yaks = oxen; // 不允许这么赋值
    yaks[SIZE] = oxen[SIZE]; // 数组的下标越界
    yaks[SIZE] = {5,3,2,8}; // 不起作用
}

2 使用指定初始化器(C99)

C99 的新特性:指定初始化器(disignated initializer)

int arr[6] = {0,0,0,0,0,0,2}; // 传统的语法

C99 规定,可以初始化列表中使用带方括号的下标指明待初始化的元素。

int arr[6] = {[5] = 2}; // 把arr[5] 初始化为2

对于一般的初始化,在初始化一个元素后,末初始化的元素的都会被设置为 0 。

⚠️ 注意:在使用数组时,要防止数组下标超出边界。必须确保下标是有效的值

3 多维数组

3.1 初始化二维数组

初始化二维数组是建立在初始化一维数组的基础上。

初始化一维数组的格式如下:

数据类型 arr[5] = {val01,val02,val03,val04,val05}; // val01、val02等表示数据类型的值。

初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化结果就不会出错。

如果初始化的数值不够,按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为 0。

初始化二维数组的两种方法:

./初始化二维数组的两种方式.png

3.2 声明二维数组

type int arr4[4]; // arr4 是一个内含 4 个 int 的数组
typedef arr4 arr3x4[3]; // arr3x4 是一个内涵 3 个 arr4 的数组

int (* ptr)[4]; // ptr 是指向 int[4] 的指针
// 以上声明相当于 int[4] *ptr,也相当于 arr4 *ptr;

二维数组作为形参时候,arr[][cols],第一个[]的值(行数)被省略,所以需要传一个 int rows 的参数

void printArr(int arr[][4], int rows);

3.3 其他多维数组

声明一个三维数组:

int  box[10][20][30]; // 10个二维数组(每个二维数组都是20行30列)

对于多维数组的处理,使用多重循环控制来处理。

4 数组和指针

指针以符号形式使用地址。

数组表示法也是变相使用指针

  • 指针的值是所指向对象的地址。地址的表示方式依赖于计算机内部的硬件。
  • 在指针前面使用运算符可以得到该指针所指向对象的值。
  • 指针加 1,指针的值递增它所指向类型的大小(以字节为单位)。

4.1 指针的兼容性

指针不允许类型转换,禁止以下操作 int_ptr = (float)int_ptr

赋值操作必须是相同类型的指针

4.2 const ptr

non const ptr 可以赋值给 const ptr,前提是只做一级解引用,二级解引用的结果是未定义的

const ptr 赋值给 non const ptr,这个行为在 c 中未定义(warning),cpp 不允许(error)

4.3 数组指针和指针数组的概念理解

数组指针:数组是修饰词,指针是名词,本质上是指针,可以理解为数组的指针,如:int (* arr) [10] 就是一个数组指针,相当于

arr [ ] [ 10 ]

指针数组:指针是修饰词,数组是名词,本质上是数组,可以理解为存放指针( int _ )的数组,如:int_ arr[10]; 一个指针数组。

4.4 指针和数组的区别

// 参数本质是指针,指针是指向 int[4],也就是二维数组,相当于 int[][4]
// 这里编译器会把第一个[]中的值给忽略,所以需要多传递一个 int rows 参数,这里涉及可变长数组,后续再讲
void someFunction(int (* ptr)[4])
void someFunction(int [][4])
void someFunction(int (* ptr)[4], int rows)

指针和数组联系紧密,大多数时候数组形式和指针形式通用

重要的区别是数组名是常量,而指针名是变量

这一点在字符串的使用中,体现尤为明显

const char * pt1 = "Something is pointing at me.";
const char ar1[] = "Something is pointing at me.";

以上声明的区别

首先字符串常量在静态存储区中

数组形式的声明是将字符串字面量,在程序运行期间拷贝到数组容器中(内存段中),这里特别要注意,拷贝后才解析数组首元素的地址赋值给 arr1,此时,arr1 是常量,可以 arr1+1,不能++arr1

指针形式的声明是将字符串字面量的首元素地址,在程序运行期间给到变量 pt1,这里注意,并没有发生拷贝字符串字面量的行为,并且 pt1 是变量,可以进行 ++pt1 这样的递增操作;最重要的一点,静态存储区是不可变的,所以一般把指针声明为 const ptr,如果不是 const,则更改 *ptr 对应的值,会让指针指向别的地址,结果是未知的

5 函数、数组和指针

使用数组表示法处理数组的函数,也就是使用指针作为参数。

数组名是数组首元素的地址。

既可以用指针表示数组名,也可以使用数组名表示指针

6 指针操作

# include<stdio.h>
int main(void)
{
    int urn[5] = {10,20,30,40,50};
    int *ptr1, *ptr2, *ptr3;

    ptr1 = urn; // 指向数组的首个元素,也就是urn数组的首地址
    ptr2 = &urn[2];// 一个地址赋给指针,也就是urn数组的第三个元素(urn[2])的地址

    printf("指针的值 ;间接引用指针 ; 指针的地址:\n");
    // 解引用:*运算符给出指针指向地址上储存的值。注意:不要解引用未初始化的指针。
    // 取址:指针也有主机的地址和值。&运算符给出指针本身的地址。
    printf("ptr1 = %p ; *ptr1 = %d ; &ptr1 = %p \n", ptr1,*ptr1,&ptr1);

    // 指针的加法:整数都会与指针所指向类型的大小(以字节为单位)相乘,然后结果与初始地址相加。
    ptr3 = ptr1 + 4;
    printf("\n 指针加上一个整数 :\n");
    printf("ptr1 + 4 = %p ; *(ptr1 + 4) = %d \n",ptr1+4,*(ptr1+4));

    ptr1++; // 递增指针:指向数组元素的指针可以让指针移动到数组的下一个元素。
    printf("\n ptr1++的值是:\n");
    printf("ptr1 = %p ; *ptr1 = %d ; &pt1 = %p \n",ptr1,*ptr1,&ptr1);

    ptr2--; // 递减指针
    printf("--ptr2的值是:\n");
    printf("ptr2 = %p ; *ptr2 = %d ; &pt2 = %p\n",ptr2,*ptr2,&ptr2);

    --ptr1; // 恢复为初始值
    ++ptr2; // 恢复为初始值

    printf("\n 恢复为初始值的指针为:\n");
    printf("ptr1 = %p ; ptr2 = %p\n",ptr1,ptr2);

    // 一个指针减去另一个指针
    printf("\n 一个指针减去另一个指针:\n");
    printf("ptr2 = %p ; ptr1 = %p ; ptr1 - ptr2 = %td \n ",ptr1,ptr2,ptr1 -ptr2);

    // 一个指针减去一个整数:指针必须是第一个运算对象,整数是第二个运算对象。整数乘以指针指向类型的大小(以字节为单位),然后初始地址减去乘积。
    printf("\n 一个指针减去一个整数:\n");
    printf("ptr3 = %p ; ptr3 - 2 = %p \n ",ptr3,ptr3-2);

    return 0;
}

7 保护数组中的数据

地址传递往往会导致会出现一些问题,原始数据在传递过程中会被修改。

define 可以创建类似 const 功能的符号常量,但由于安全性角度考虑,优先推荐使用 const定义常量。

指针 const 的指针不能用于改变值。

对函数的形参使用 const 不仅能保护数据,还能让函数处理 const 数组。

8 c/cpp 关于 const 的区别

cpp 允许使用 const int 作为数组的长度,c 不允许

cpp 对于 const ptr 赋值的校验很严格, non const ptr = const ptr ,这个行为在 c 中未定义(warning),cpp 不允许(error)

9 变长数组(VLA)

⚠️ 注意:变长数组不能改变大小

变长数组中的“变” 不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。此处的“变” 指的是:在创建数组时,可以使用变量指定数组的维度。

int rows = 4;
int cols = 5;
double arr4x5[rows][cols];

C99/C11 标准规定,可以省略原型中的形参名,可以使用星号来代替省略的维度

int num2d(int, int , int arr[*][*]); // arr是一个变长数组(VLA),省略了维度形参名。

变长数组名实际上也是指针

变长数组必须是自动存储类型

变长数组允许动态内存分配,说明可以在程序运行时指定数组的大小

tips: 常量和变量的区别,常量是编译时期就能确定值,变量需要等到运行时期才能确定值

变长数组作为函数的参数时,注意参数顺序问题

void vlaSum(int rows, int cols, double arr4x5[rows][cols]); // 正确
void vlaSum(double arr4x5[rows][cols], int rows, int cols); // 错误