首页 > 其他分享 >C 数组

C 数组

时间:2024-04-24 17:24:34浏览次数:21  
标签:arr 示例 int 数组 长度 指针

创建数组

数组是一组相同类型的值,按照顺序储存在一起。数组通过变量名后加方括号表示,方括号里面是数组的成员数量。

int arr[100];

上面示例声明了一个数组arr,里面包含100个成员,每个成员都是int类型。注意,声明数组时,必须给出数组的大小。
数组的成员从0开始编号,所以数组arr[100]就是从第0号成员一直到第99号成员,最后一个成员的编号会比数组长度小1

数组名后面使用方括号指定编号,就可以引用该成员。也可以通过该方式,对该位置进行赋值。

arr[0] = 13;
arr[99] = 42;

上面示例对数组arr的第一个位置和最后一个位置,进行了赋值。

注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,可能读取或写入预期外内存,非常危险,必须非常小心。

int arr[100];
arr[100] = 51;	// 数组越界

上面示例中,数组arr只有100个成员,因此arr[100]这个位置是不存在的。但是,引用这个位置并不会报错,会正常运行,使得紧跟在arr后面的那块内存区域被赋值,而那实际上是其他变量的区域,因此不知不觉就更改了其他变量的值。这很容易引发错误,而且难以发现。

数组也可以在声明时,使用大括号初始化,对每一个成员赋值, 注意使用大括号初始化时,必须在数组声明时赋值,否则编译时会报错。

int arr[5] = {22, 37, 3490, 18, 95};

下面代码中,数组arr声明之后再进行大括号赋值,导致报错。

int arr[5];
arr = {22, 37, 3490, 18, 95}; // 报错

报错的原因是,C 语言规定,数组变量一旦声明,就不得修改变量指向的地址,具体会在后文解释。由于同样的原因,数组赋值之后,再用大括号修改值,也是不允许的。

int arr[5] = {1, 2, 3, 4, 5};
arr = {22, 37, 3490, 18, 95}; 	// err

使用大括号初始化,大括号里面的值不能多于数组的长度,否则编译时会报错。如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为0

// 两者相等
int a[5] = {22, 37, 3490};			// 剩余元素自动填充0
int a[5] = {22, 37, 3490, 0, 0};

如果要将整个数组的每一个成员都设置为零,最简单的写法就是下面这样,char数组比较特殊,会自动填充'\000'

int a[100] = {0};

数组初始化时,可以指定为哪些位置的成员赋值。

int a[15] = {[2] = 29, [9] = 7, [14] = 48};

上面示例中,数组的2号、9号、14号位置被赋值,其他位置的值都自动设为0。

指定位置的赋值可以不按照顺序,下面的写法与上面的例子是等价的。

int a[15] = {[9] = 7, [14] = 48, [2] = 29};

指定位置的赋值与顺序赋值,可以结合使用。

int a[15] = {1, [5] = 10, 11, [10] = 20, 21}

上面示例中,0号、5号、6号、10号、11号被赋值。

C 语言允许省略方括号里面的数组成员数量,这时将根据大括号里面的值的数量,自动确定数组的长度。

// 两者相等
int a[] = {22, 37, 3490};
int a[3] = {22, 37, 3490};

上面示例中,数组a的长度,将根据大括号里面的值的数量,确定为3

省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。下面面示例中,数组a的最大下标是9,所以数组的长度是10。

int a[] = {[2] = 6, [9] = 12};

数组长度

sizeof运算符计算对象占用内存字节数,传入宿主会返回整个数组内存长度。

int a[] = {22, 37, 3490};
int arrLen = sizeof(a);	 // 12

上面示例中,sizeof返回数组字节长度是12

由于数组成员都是同一个类型,每个成员的字节长度都是一样的,所以数组整体的字节长度除以某个数组成员的字节长度,就可以得到数组的成员数量。

sizeof(a) / sizeof(a[0])

上面示例中,sizeof(a)是整个数组的字节长度,sizeof(a[0])是数组成员的字节长度,相除就是数组的成员数量。

注意,sizeof返回值的数据类型是size_t,所以sizeof(a) / sizeof(a[0])的数据类型也是size_t。在printf()里面的占位符,要用%zd%zu

int x[12];

printf("%zu\n", sizeof(x));     			// 48
printf("%zu\n", sizeof(int));  				// 4
printf("%zu\n", sizeof(x) / sizeof(int)); 	// 12

上面示例中,sizeof(x) / sizeof(int)就可以得到数组成员数量12

数组指针无法获取数组长度,这是数组指针和数组名重要的区别之一

int x[12];
int *p = &x;

printf("%zu\n", sizeof(x));		// 48
printf("%zu\n", sizeof(p));		// 8

数组名称

数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。请看下面的例子。

int arr[5] = {11, 22, 33, 44, 55};

int* p = &arr[0];
printf("%d\n", *p);  // Prints "11"

上面示例中,&a[0]就是数组a的首个成员11的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p),可以获得首个成员的值11

由于数组的起始地址是常用操作,&array[0]的写法有点麻烦,C 语言规定数组名等同于起始地址,也就是说数组名就是指向第一个成员array[0] 的指针常量。

int *p_arr = &arr[0];

*p_arr == &arr[0];		// true
arr[n] == *(arr + n);	// true

上面示例中等式总是成立

把数组名当指针使用,也可以取出变量值。看起来数组名和指针有相同的特性,其实不然,两者是有本质区别的,只有部分场景可以混用,其它章节会有说明

// 取出第一个元素
printf("%d\n", *a); 	// 11

// 移动指针到下一个元素
printf("%d\n", *(a+1)); // 22

如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。函数接受数组作为参数,函数原型可以写成下面这样。

// 写法一
int sum(int arr[], int len);
// 写法二
int sum(int *arr, int len);

上面示例中,传入整数数组,与传入整数指针两者等价

示例通过数组指针对成员求和,函数形参是指针类型,通过指针获取数组的每个成员。

int sum(int *arr, int len) {
  int i;
  int total = 0;

  // 通过下标遍历数组
  for (i = 0; i < len; i++) {
    total += arr[i];			
  }
  return total;
}

上面案例可以看出,指针也支持下标方式访问元素

数组名也支持运算,可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址

int arr[5] = {11, 22, 33, 44, 55};

for (int i = 0; i < 5; i++) {
  printf("%d\n", *(arr + i));
}

数组名还有个重要特性,指向的地址是不能更改。声明数组时,编译器自动为数组分配了内存地址,这个地址与数组名是绑定的,不可更改,下面的代码会报错。

int ints[100];
ints = NULL; // 报错

*&运算符也可以用于多维数组。

int arr[4][2];

*(arr[0]);		// 取出 a[0][0] 的值
**a				// 等同于

上面示例中,由于a[0]本身是一个指针,指向第二维数组的第一个成员a[0][0]。所以,*(a[0])取出的是a[0][0]的值。至于**a,就是对a进行两次*运算,第一次取出的是a[0],第二次取出的是a[0][0]。同理,二维数组的&a[0][0]等同于*a

数组指针

数组是一段连续的内存地址,数组名是保存第一个元素内存地址的常量,指针也是指向内存起始地址,所以以下等式总是成立

int arr[] = {1, 2, 3};

int *p_int = arr;	// 普通指针
p_int == &arr[0]	// true

数组名的这个特性导致一些有趣特性,数组指针是指向数组起始地址,普通指针也是指向元素的起始地址,两者虽然是不同类型,有些场景可以混用和替换,因为都指向 int 类型的内存地址。

普通指针也可以访问数组,三种方式都可以,如下案例

p_int[0]; 
*p_int
*(p_int+0);

注意,普通指针也支持下标方式读取元素

数组指针,专门用于指向数组的指针,与普通指针有所区别。如下申明指针数组,指向数组首地址

int arr[3] = {1,2,3}		// 数组
int (*p_arr)[3] = &arr; 	// 数组指针,指向数组首地址

申明语句int* p_arr[3],中括号[]优先级更高先和p_arr结合,表示p_arr是长度为3的整型数组,再结合*结合,表示长度为3个整型指针数组。需要使用小括号()改变优先级,使*p_arr先结合,表示p_arr是个指针, 再结合[],表明是个指向长度为3的数组指针
注意:这里有两个概念容易混淆

  • 数组指针:类型是指针,指向数组的指针;
  • 指针数组:类型是数组,存储的每个元素都是指针

使用数组指针访问元素,看起来麻烦一些.

(*p_arr)[0];
*p_arr[0];
*(*p_arr+0)

普通指针 和 数组指针都指向数组首地址,如下等式总是成立,它们有部分特性相似,比如都可以读取数组内容,但有本质区别。

p_int == p_arr;	 // true

在元素访问上区别,申明方式不同,编译器解析方式有区别对待,数组指针维护有数组长度信息,如下案例

sizeof(*p_int);		// 4, 获取目标类型是int,32位系统下长度是4;
sizeof(*p_arr)		// 12, 获取目标类型是长度3的int数组,长度是 4 * 3 = 12;

还有重要区别是指针运算,步长是指向数据类型的长度,所以数组指针的步长是整个数组的长度。

p_int+1;	// p = p+4
p_arr+1;	// p2 = p2+12

p_int 表示int的长度,p_arr表示整个数组的长度。日常更多的使用方式是普通指针,通过指针预算可以更细粒度控制

遍历数组也有区别

int arr[] = {1, 2, 3};


int *p_int = arr;			// 普通指针
int (*p_arr)[3] = &arr;		// 数组指针

for(i=0; i<3; i++) {
    printf("%d\n", *(p_int+i));		// 普通指针, 等价printf("%d\n", p_int[i]);
    printf("%d\n", *p_arr)[0]);		// 数组指针
}

另外,遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现。

int sum(int* start, int* end) {
  int total = 0;

  while (start < end) {
    total += *start;
    start++;
  }
  return total;
}

int arr[5] = {20, 10, 5, 39, 4};
printf("%i\n", sum(arr, arr + 5));

上面示例中,arr是数组的起始地址,arr + 5是结束地址。只要起始地址小于结束地址,就表示还没有到达数组尾部。

反过来,通过数组的减法,可以知道两个地址之间有多少个数组成员,请看下面的例子,自己实现一个计算数组长度的函数。

int arr[5] = {20, 10, 5, 39, 88};
int* p = arr;

while (*p != 88)
  p++;

printf("%i\n", p - arr); // 4

上面示例中,将某个数组成员的地址,减去数组起始地址,就可以知道,当前成员与起始地址之间有多少个成员。

同一个数组的两个成员的指针相减时,返回它们之间的距离。

int* p = &a[5];
int* q = &a[1];

printf("%d\n", p - q); // 4
printf("%d\n", q - p); // -4

上面示例中,变量pq分别是数组5号位置和1号位置的指针,它们相减等于4或-4。

对于多维数组,数组指针的加减法对于不同维度,含义是不一样的。

int arr[4][2];

// 指针指向 arr[1]
arr + 1;

// 指针指向 arr[0][1]
arr[0] + 1

上面示例中,arr是一个二维数组,arr + 1是将指针移动到第一维数组的下一个成员,即arr[1]。由于每个第一维的成员,本身都包含另一个数组,即arr[0]是一个指向第二维数组的指针,所以arr[0] + 1的含义是将指针移动到第二维数组的下一个成员,即arr[0][1]

数组名、普通指针、数组指针的特性

类型 申明方式 sizeof 长度&运算步长 指向地址 只读
数组 int arr[3] 12 首地址
普通指针 int *p_int = arr 4 首地址
数组指针 int (*p_arr)[3] = arr 12 首地址

数组复制

由于数组名是指针,所以复制数组不能简单地复制数组名

int b[3] = {1, 2, 3};
int a[3] = b;			// err
int *p_int = b;			// 指针

复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制。

for (i = 0; i < N; i++) {
  a[i] = b[i];
}

上面示例中,通过将数组b的成员逐个复制给数组a,从而实现数组的赋值。

另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份。

memcpy(a, b, sizeof(b));

上面示例中,将数组b所在的那段内存,复制给数组a。这种方法要比循环复制数组成员要快。

变长数组

数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)。c99 标准中,新增了可变长度数组;C11 中 VLA 变为可选项,不是语言必备的特性。
变长数组中的 “变” 不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小则保持不变。这里的 “变” 指的是在创建数组时,可以使用变量指定数组的长度。(普通数组只能用常量或常量表达式指定数组的长度)

  1. 变长数组 VLA 只能是局部变量数组
  2. 变长数组 VLA 不能在定义的时候进行初始化
  3. 变长数组 VLA 必须是自动存储类别,即不能使用 extern 或 static 存储类别说明符
  4. 变长数组 VLA 不等于动态数组,本质还是静态数组,也就是说,数组的长度在变量的整个生命周期中是不可变的
  5. 由于变长数组只能是局部变量,且必须是自动存储类别,因此变长数组分配在栈上
  6. 可变长数组对于多维数组也适用(如 array[a][b] )
int n = x + y;
int arr[n];

上面示例中,数组arr就是变长数组,因为它的长度取决于变量n的值,编译器没法事先确定,只有运行时才能知道n是多少。

变长数组的根本特征,就是数组长度只有运行时才能确定。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。任何长度需要运行时才能确定的数组,都是变长数组。

int i = 10;

int a1[i];
int a2[i + 5];
int a3[i + k];

上面示例中,三个数组的长度都需要运行代码才能知道,编译器并不知道它们的长度,所以它们都是变长数组。

变长数组也可以用于多维数组,下面示例中c[m][n]就是二维变长数组。

int m = 4;
int n = 5;
int c[m][n];

使用 **malloc **也实现编程数组,详见内存管理章节

char数组

C语言中没有字符串类型,使用char数组表示字符串,为了兼容部分场景,所以char数组比较特殊,有一些特有属性,可能会自动初始化,但并不可靠,要看操作系统分配内存机制

// 所有元素初始化 \0 (ASCII码第一个字符, 十六进制0x00, 不是数值0)
char chars[10];

// 建议显示初始化
char chars[10] = {0};

其他类型数组是随机值

使用 \0 表示字符串结尾,并会自动添加

char chars[] = "hello";

sizeof(chars); // 自动尾部添加0x00, 所以字符串长度是6 (hello + \0 = 6)  

printf打印字符串时候,逐个读取字符,遇到\0表示到达尾部,停止读取,打印输出

如下只能打印部分内容

char chs[5] = {[0]='a', [1]='b', [3]='c'};
printf("%s\n", chs); // 输出ab

因为chs[2] 初始化为\0

长度不够,无法自动添加\0

char chars[5] = "hello";
printf("%s\n", chars);  // 打印会乱码

添加\0失败,缺少标志位,printf打印异常

其他情况

char str1[] = {'a', 'b'}; 	// 乱码, 缺少尾部\0

char str2[] = {"hello"};	// 正常
char str3[] = "hello";      // 正常

字符指针可以使用字面量初始化,这种写法其他类型不允许

char* str = "hello";
printf("%s\n", str);  // 自动添加\0, 正常打印

多维数组

C 语言允许声明多个维度的数组,有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号。

int board[10][10];

上面示例声明了一个二维数组,第一个维度有10个成员,第二个维度也有10个成员。多维数组可以理解成,上层维度的每个成员本身就是一个数组。比如上例中,第一个维度的每个成员本身就是一个有10个成员的数组,因此整个二维数组共有100个成员(10 x 10 = 100)。

三维数组就使用三个方括号声明,以此类推。

int c[4][5][6];

引用二维数组的每个成员时,需要使用两个方括号,同时指定两个维度。

board[0][0] = 13;
board[9][9] = 13;

注意,board[0][0]不能写成board[0, 0],因为0, 0是一个逗号表达式,返回第二个值,所以board[0, 0]等同于board[0]。跟一维数组一样,多维数组每个维度的第一个成员也是从0开始编号。

多维数组也可以使用大括号,一次性对所有成员赋值。

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

上面示例中,a是一个二维数组,这种赋值写法相当于将第一维的每个成员写成一个数组。这种写法不用为每个成员都赋值,缺少的成员会自动设置为0

多维数组也可以指定位置,进行初始化赋值。

int a[2][2] = {[0][0] = 1, [1][1] = 2};

上面示例中,指定了[0][0][1][1]位置的值,其他位置就自动设为0

不管数组有多少维度,在内存里面都是线性存储,a[0][0]的后面是a[0][1]a[0][1]的后面是a[1][0],以此类推。因此,多维数组也可以使用单层大括号赋值,下面的语句与上面的赋值语句是完全等同的。

int a[2][2] = {1, 0, 0, 2};

作为函数的参数

声明参数数组

数组作为函数的参数,一般会同时传入数组名和数组长度。

int sum_array(int a[], int n) {
  // ...
}

int a[] = {3, 5, 7, 3};
int sum = sum_array(a, 4);

上面示例中,函数sum_array()的第一个参数是数组本身,也就是数组名,第二个参数是数组长度。由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入。

如果函数的参数是多维数组,那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。

int sum_array(int a[][4], int n) {
  // ...
}

int a[2][4] = {
  {1, 2, 3, 4},
  {8, 9, 10, 11}
};
int sum = sum_array(a, 2);

上面示例中,函数sum_array()的参数是一个二维数组。第一个参数是数组本身(a[][4]),这时可以不写第一维的长度,因为它作为第二个参数,会传入函数,但是一定要写第二维的长度4

这是因为函数内部拿到的,只是数组的起始地址a,以及第一维的成员数量2。如果要正确计算数组的结束地址,还必须知道第一维每个成员的字节长度。写成int a[][4],编译器就知道了,第一维每个成员本身也是一个数组,里面包含了4个整数,所以每个成员的字节长度就是4 * sizeof(int)

变长数组作为参数

变长数组作为函数参数时,写法略有不同。数组a[n]是一个变长数组,它的长度取决于变量n的值,只有运行时才能知道。所以变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]的长度,否则就会报错。实参a其实是一个指针,不同于数组名无法使用sizeof(a)获取长度。

int sum_array(int n, int a[n]) {
  // ...
}

int a[] = {3, 5, 7, 3};
int sum = sum_array(4, a);

因为函数原型可以省略参数名,所以变长数组的原型中,可以使用*代替变量名,也可以省略变量名,下面两种变长函数的原型写法,都是合法的。

int sum_array(int, int [*]);
int sum_array(int, int []);

变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。

// 原来的写法
int sum_array(int a[][4], int n);

// 变长数组的写法
int sum_array(int n, int m, int a[n][m]);

上面示例中,函数sum_array()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。

数组字面量作为参数

C 语言允许将数组字面量作为参数,传入函数。

// 数组变量作为参数
int a[] = {2, 3, 4, 5};
int sum = sum_array(a, 4);

// 数组字面量作为参数
int sum = sum_array((int []){2, 3, 4, 5}, 4);

上面示例中,两种写法是等价的。第二种写法省掉了数组变量的声明,直接将数组字面量传入函数。{2, 3, 4, 5}是数组值的字面量,(int [])类似于强制的类型转换,告诉编译器怎么理解这组值。


参考
C语言的变长数组
详解C语言变长数组
C语言中数组名和指针的区别

标签:arr,示例,int,数组,长度,指针
From: https://www.cnblogs.com/asdfzxv/p/18155907

相关文章

  • 数组处理,去重&合并相同key的值
    背景:上游返回的skuIdList存在相同的Id,skuCount数组与skuID一一对应处理结果:skuIdList去重,对应的count总和要加起来,对应的originalPriceList和subtotalPriceList不变代码:importgroovy.json.JsonSlurperdefskuIdList=newJsonSlurper().parseText('["472262851","472......
  • Qt short int 数组大小端转换(qbswap)
    在以下情形中,展示了如何将 shortint 数组从大端序转换为小端序,或者从小端序转换为大端序。1#include<iostream>2#include<cstdint>3intmain()4{5constexprintsize=4;6std::uint16_tarr[size]={0x1234,0x5678,0x9abc,0xdef0};7/......
  • JS基础(二)运算符、流程控制语句、数组对象、JSON对象、Date对象、Math对象、Function对
    一运算符<script>//算数运算符//(1)自加运算varx=10;//x=x+1;//x+=2;varret=x++;//先赋值再计算:x+=1//varret=++x;//先计算再赋值:x+=1console.log(x)......
  • 手动输入一个数组,并调用函数算出数组之和
    /***********************************************************************************filename:005_数组之和.cauthor:[email protected]:2024/04/18function:手动输入一个数组,并算出数组之和note......
  • 找到一个数组中的最大最小值以及下标
    /***********************************************************************************filename:005_找到一个数组中的最大最小值以及下标.cauthor:[email protected]:2024/04/18function:找到一个数组中的最大......
  • 34.c语言数组练习题(牛客网)
    先打个广告哈哈哈牛客网练编程题不错不错哦冒泡排序必须必须必须会#include<stdio.h>voidsort(intarr[],intn){//外层循环for(inti=0;i<n-1;++i){intflag=1;//假设flag=1就是已经排序好的//内层循环for(intj=0;......
  • 树状数组
    1.0树状数组概念 【五分钟丝滑动画讲解|树状数组|21智人马同学】 树状数组用于高效计算数组前缀和及支持单点更新操作。与前缀和区别在于树状数组可以在O(logn)的时间复杂度支持单点更新操作。其数学证明参考 树状数组的基本原理 1.1树状数组的性质lowbit......
  • 6.Java数组
    Java数组数组概述相同类型数据的有序集合通过下标访问他们数组的声明与创建publicclassArrayDemo01{publicstaticvoidmain(String[]args){//变量类型变量名字=变量的值int[]nums;//1.声明一个数组首选//intnums2[]......
  • 34天【代码随想录算法训练营34期】第八章 贪心算法 part03 (● 1005.K次取反后最大化
    1005.K次取反后最大化的数组和classSolution:deflargestSumAfterKNegations(self,nums:List[int],k:int)->int:nums.sort(key=lambdax:abs(x),reverse=True)foriinrange(len(nums)):ifnums[i]<0andk>0:......
  • JavaScript 数组增强
    Javascript的数组最近通过新的原型方法(例如toReversed、toSorted、toSpliced和with)获得了新的力量。这些新方法提供了在JavaScript中更改数组的额外方​​法。它允许进行更改并获取包含这些更改的数组的新副本。 Array.prototype.toReversed:-此方法返回一个新数组,其元素顺......