【C语言】数组与字符串整理总结

数组简介

数组(容器):连续有顺序的,大小固定并且里面的数据类型一致的内存空间

  1. 数组的声明与初始化:

数据类型 标识符[长度]; int arr[3] = {1, 2, 3};

数据类型 标识符[] = {元素1,元素2…元素n}; 此时长度为n

数据类型 标识符[长度n]; 数组名称[0] = 元素1; … 数组名称[n-1] = 元素n;(不推荐)

int a[15] = {1, [5] = 10, 11, [10] = 20, 21} 若省略长度,则长度为最大索引+1

  • 数组在初始化的时候,数组内元素的个数不能大于声明的数组长度;如果引用不存在的数组成员**(即越界访问数组)**,并不会报错,所以必须非常小心
  • 第一种初始化方式,元素个数小于数组的长度时,多余的数组元素初始化为0;使用大括号赋值时,必须在数组声明时赋值,否则编译时会报错(数组变量一旦声明,就不得修改变量指向的地址)
  • 在声明数组后没有进行初始化的时候,静态(static)和外部(extern)类型的数组元素初始化元素为0,自动(auto)类型的数组的元素初始化值不确定。
  1. 数组元素的引用与赋值

数组名称[索引];

  • 数组索引下表均以0开始
  • 建议在循环中用一个临时变量来保存索引值,而不是循环引用它
  1. 获取数组长度

以int arr[10] = {0};为例

sizeof :会返回整个数组的字节长度

数组arr长度 int arrlen = sizeof(arr) / sizeof(arr[0]); 二维数组也同样适用

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

  1. 数组的遍历
# include 
int main(){
	// 假设这个数组是传递过来的,长度未知
	int arr[] = {1, 2, 3};
	size_t size = sizeof(arr) / sizeof(arr[0]);

	for (int i = 0; i < size; i++){
		printf("arr[%d]: %d\n",i, arr[i]);
	}
	return 0;
}
  • 避免出现数组越界访问,循环变量最好不要超出数组的长度; C语言是没有检查数组长度改变或者数组越界的这个机制
  • C语言的数组长度一经声明,长度就是固定,无法改变,并且C语言并不提供计算数组长度的方法
  1. 变长数组(C语言新引入的特性)

数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)。

int n = x + y;
int arr[n];

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

变长数组也可用于多维数组。

多维数组

有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号。

  1. 多维数组的初始化
int board[10][10];
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};

  1. 多维数组元素的引用

多维数组可以理解成,上层维度的每个成员本身就是一个数组。

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

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

board[0][0]不能写成board[0, 0]因为0, 0是一个逗号表达式,返回第二个值,所以board[0, 0]等同于board[0]

跟一维数组一样,多维数组每个维度的第一个成员也是从0开始编号。

数组与指针、地址

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

起始地址:&a[0] 或者数组名a,都指向数组起始元素a[0]

  1. 数组做为函数实参

把数组传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。

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

上面示例中,传入一个整数数组,与传入一个整数指针是同一回事,数组符号[]与指针符号*是可以互换的。

由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以最好也把数组长度也一起传入。

数组元素作为函数实参传递时,数组元素类型必须与形参数据类型一致。

  • 二维数组传参
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)

  1. 变长数组作为参数

变长数组作为函数参数时,写法略有不同。

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

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

上面示例中,数组a[n]是一个变长数组,它的长度取决于变量n的值,只有运行时才能知道。所以,变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]的长度,否则就会报错。

因为函数原型可以省略参数名,所以变长数组的原型中,可以使用*代替变量名,也可以省略变量名。

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()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。

数组字面量作为参数

// 数组变量作为参数
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 [])类似于强制的类型转换,告诉编译器怎么理解这组值。(我没有见过这种强制类型转换的,到时有过int

  1. 多维数组的地址
int a[4][2];

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

**a[0]本身是一个指针,等价&a[0][0]以及*a,指向第二维数组的第一个成员a[0][0]**a,就是对a进行两次*运算,第一次取出的是a[0],第二次取出的是a[0][0]。同理,二维数组的&a[0][0]等同于*a。**

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

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

  • 这也导致不能将一个数组名赋值给另外一个数组名。

    int a[5] = {1, 2, 3, 4, 5};
    // 写法一
    int b[5] = a; // 报错
    // 写法二
    int b[5];
    b = a; // 报错

  1. 数组指针的加减法

C 语言里面,数组名可以进行加法和减法运算,**等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址。**比如,a + 1返回下一个成员的地址,a - 1返回上一个成员的地址。

数组指针的加减法只能在同一个数组中进行,不同数组间的加减法的结果是未定义的,也是没有意义的

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

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

*由于数组名与指针是等价的:a[b] 等价 (a + b) 也等价b[a] 少用

如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组。

int a[] = {11, 22, 33, 44, 55, 999};

int* p = a;

while (*p != 999) {
  printf("%d\n", *p);
  p++;
}

数组名指向的地址是不能变的所以上例中,不能直接对a进行自增,即a++的写法是错的,必须将a的地址赋值给指针变量p,然后对p进行自增

通过数组的减法,可以知道两个地址之间有多少个数组成员

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

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

int arr[4][2];

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

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

数组的复制

int* a;
int b[3] = {1, 2, 3};
a = b; // 错误
/*
	**由于数组名是指针,所以复制数组不能简单地复制数组名。
	上面的写法,结果不是将数组b复制给数组a,而是让a和b指向同一个数组。**
*/
  1. 循环复制数组

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

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

  1. 内存空间复制数组

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

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

上面示例中,将数组b所在的那段内存,复制给数组a这种方法要比循环复制数组成员要快。(但是要确保a的内存长度大于等于b的内存长度)

字符串数组简介

C 语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}处理的。

在字符串结尾,C 语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的 ASCII 码是0(二进制形式00000000,即数字0),后者的 ASCII 码是48(二进制形式00110000)。)所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}

char localString[10];

声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。

简单写法

{‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’} // 等价于"Hello"

双引号里面的字符串,不用自己添加结尾字符\0,C 语言会自动添加。

注意,双引号里面是字符串,单引号里面是字符,两者不能互换。

  • 转义字符与字符串的简单扩展

    1. 如果字符串内部包含双引号,则该双引号需要使用反斜杠转义。
      “She replied, “It does.””

    2. 反斜杠还可以表示其他特殊字符,比如换行符(\n)、制表符(\t)等。
      “Hello, world!\n”

    3. 如果字符串过长,可以在需要折行的地方,使用反斜杠(\)结尾,将一行拆成多行。

      “hello
      world”

      这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。

    为了解决 4 这个问题,C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。

    char greeting[50] = "Hello, ""how are you ""today!";
    // 等同于
    char greeting[50] = "Hello, how are you today!";
    
    
    char greeting[50] = "Hello, "
      "how are you "
      "today!";
    

    printf()使用占位符%s输出字符串。

    printf("%s\n", "hello world")
    

字符串变量

  • 字符串变量的声明

字符串变量可以声明成字符数组。

char s[13] = “Hello,world!”;

也可以声明成指针,指向字符数组。

char* s = “Hello,world!”;

如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。char s[] = “Hello,world!”;

  • 字符串长度

字符数组的长度,可以大于字符串的实际长度。

实际长度为6,6之后的都会被初始化成\0

char s[50] = "hello"; 此时,strlen(s)为5,sizeof(s)为50

字符数组的声明长度,不能小于字符串的实际长度。

编译器会报错;省略结尾符号\0,这很可能导致后面的字符串相关代码出错

char s[5] = “hello”;

  • 字符指针和字符数组的差异

第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。修改后会导致难以预测的后果,执行时很可能会报错。

为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const说明符,保证该字符串是只读的。

字符串一旦尝试修改,编译器肯定会报错。(不会修改成功,难以预测的后果也不会出现)

const char* s = “Hello, world!”;

char* s = “Hello, world!”;
s[0] = ‘z’; // 错误

如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。

char s[] = “Hello, world!”;
s[0] = ‘z’;

声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。

但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。

第二个差异是,指针变量可以指向其它字符串。但字符串数组不可以指向其他字符串。

字符指针可以指向另一个字符串。

char* s = “hello”;

char *p = “nihao”;
s = “world”; // 正确

s = p; // 正确 将一个指针的地址复制给另一个指针

可以!

字符数组的数组名,总是指向初始化时的字符串地址,不能修改。不能直接用字符串赋值。

char s[] = “hello”;
s = “world”; // 报错

char s[10];
s = “abc”; // 错误

原因是数组变量所在的地址无法改变,或者说,编译器一旦为数组变量分配地址后,这个地址就绑定这个数组变量了,这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。

想要重新赋值,必须使用strcpy,数组变量的地址还是不变的,strcpy()只是在原地址写入新的字符串,而不是让数组变量指向新的地址。

char s[10];
strcpy(s, “abc”);

  • 二维字符串

    如果一个数组的每个成员都是一个字符串,需要通过二维的字符数组实现。每个字符串本身是一个字符数组,多个字符串再组成一个数组。

    char weekdays[7][10] = {
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };
    

    上面示例就是一个字符串数组,一共包含7个字符串,所以第一维的长度是7。其中,最长的字符串的长度是10(含结尾的终止符\0),所以第二维的长度统一设为10。

    因为第一维的长度,编译器可以自动计算,所以可以省略。

    不可以!只有最左边的数字下表可以省略

    char weekdays[][10] = {
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };
    

    上面示例中,二维数组第一维的长度,可以由编译器根据后面的赋值,自动计算,所以可以不写。

    数组的第二维,长度统一定为10,有点浪费空间,因为大多数成员的长度都小于10。解决方法就是把数组的第二维,从字符数组改成字符指针。

    char* weekdays[] = { 
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };
    

    上面的字符串数组,其实是一个一维数组,成员就是7个字符指针,每个指针指向一个字符串(字符数组)。[] 是第一维的, 是第二维的*

    遍历字符串数组的写法如下。

    for (int i = 0; i < 7; i++) {
      printf("%s\n", weekdays[i]);
    }
    

    char **strings,这个是二维指针

    char (*string)[ ] 这个是什么

你可能感兴趣的:(c语言)