数组:一组具有相同数据类型的数据的集合
1) 一维数组
2) 二维数组
3) 字符数组
1. 一维数组
定义格式:类型说明符 数组名[整数表达式];
“类型说明符”:指定数组元素的类型,任意C语言合法的类型都可以
“数组名”:遵循C语言标识符命名规则,是一个常量,其值为首元素的地址
“整数表达式”(整型常量、符号常量(#define)、常量表达式):指定数组元素的个数。现在也可以是变量、变量表达式,并且变量、变量表达式必须有明确的值。如果是变量、变量表达式则数组不能初始化eg:
int a[10]; // 定义了一个有10个元素的数组 a,每一个元素都是 int 型一维数组在内存中存放:
在连续的地址空间,从低地址到高地址依次存放数组中每一个元素一维数组元素的引用:只能引用数组元素而不能一次整体调用整个数组全部元素的值
数组名[下标]
“下标”:既可以是整型常量、符号常量、常量表达式也可以是变量、变量表达式,但必须是>=0的整数,有明确的值
在C语言中,下标是从0开始的。最大值是 数组大小-1
a[0]、a[1]、... a[n-1]
引用数组元素 a[i] 跟普通变量一样,即可以作为左值也可以作为右值,还可以取地址
一维数组的初始化:数组的初始化用 { } ,并且必须在定义时直接初始化
(1) 全部初始化
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
(2) 可以对部分元素初始化,后面的元素就自动初始化为0
int a[10] = {1, 2, 3, 4, 5};
(3) 如果对全部数组元素赋初值,那么可以不指定数组长度(元素个数)
int a[] = {1, 2, 3, 4, 5, 6};eg:
int a[100] = {0}; // 一般用于数组元素清0
#include <stdio.h>
int main(int argc, char *argv[]) {
int s[12] = {1, 2, 3, 4, 4, 3, 2, 1, 1, 1, 2, 3}, c[5] = {0}, i;
for(i = 0; i < 12; i++) {
c[s[i]]++;
}
for(i = 1; i < 5; i++) {
printf("%d ", c[i]); // 4 3 3 2
}
printf("\n");
return 0;
}
2. 二维数组
数组元素可以出现在表达式中,也可以被赋值
例如: b[1][2] = a[2][3] / 2
int a[4]; // 定义了一个数组a,有4个元素,每个元素都是int类型
再定义一个数组 b typeof(a) <===> int [4]
typeof(a) b[3]; // 定义了一个数组b,有3个元素,每个元素都是 int[4]类型
==> int[4] b[3];
==> int b[3][4];
由推导过程可以得出:二维数组实际上就是一个一维数组,只不过该一维数组的元素又是
一个一维数组罢了
我们再实际应用中,通过把 int b[3][4] 看成一个3行4列的矩阵
二维数组定义:
类型说明符 数组名[整数表达式][整数表达式];
多少行 每行多少列
二维数组在内存中的存放:
按行存放,即先顺序存放第一行的元素,再放第二行的元素...
二维数组的引用:
数组名[第几行][第几列]
二维数组的初始化:用{}实现初始化,且在定义时直接初始化
(1) 分行给二维数组赋初值
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
(2) 将所有数值写在一个花括号里,按数组的排序顺序对元素依次赋初值
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
(3) 只对部分元素赋初值,其余的元素自动置0
int a[3][4] = {0}; 等价于 int a[3][4] ={ };
int a[3][4] = {1, 2, 3};
int a[3][4] = {{1, 2, 3}, {5, 6}, {9, 10, 11, 12}}; (4) 如果对全部元素都赋初值,则定义数组时可以对第一维的长度省略
int a[ ][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};二维数组中其实是一个线性数组存放着 其他数组的首地址
3. 字符数组及字符串操作函数
1)C语言中没有字符串类型,也没有字符串变量,字符串是存放在字符数组中的
2)用"%s"格式符输出字符串时,printf函数中的输出项是字符数组名,而不是数组元素名 例:printf("%s", c); // (一次整体调用整个数组全部元素的值)
3)如果初值个数小于数组长度,则只将这些字符赋给数组中前面那些元素,其余的元素自动定为空字符('\0')
4) 在遇到字符 '\0' 时,表示字符串结束,把它前面的字符组成一个字符串
5)中文也是字符
字符数组,就是数组元素是 char(字符型)的数组,它可以一维的,也可以是二维的
char c1[5] = {'c', 'h', 'i', 'n', 'a'};
char c2[6] = {"china"} 等价于 char c2[6]= "china";
两者有什么区别?
char c2[] = "china";
<==> char c2[] = {'c', 'h', 'i', 'n', 'a', '\0'}; // 多一个'\0'字符串在保存时会在末尾加一个终止符 '\0','\0'的ASCII为0,终止符的作用就是用来标识字符串的结束
我们在进行操作字符串都是带入字符串的首地址,正因为每个字符串都会有一个终止符,所有系统才会知道字符串的具体范围
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char str[3][3] = { {'1', '2', '3'}, {'1', '2', '3'}, {'1', '2', '3'}, }; printf("%s\n", str[1]); // 123123 return 0; }
数组名a:
1. 代表整个数组
sizeof(a),typeof(a),a (数组名是一个指针常量,是一个指针类型的常量,指向数组的首地址,不能修改),&a (整个数组的地址)
2. 当指针用
a+1 ===>&a[0] + 1 ===>&a[1]
*a
正确:c h i n a #include <stdio.h> int main(int argc, char *argv[]) { char c[] = "china"; char *p = c; while (*p) { // while (*p != '\0') printf("%c ", *p++); } return 0; } 错误:p越界但不指向空 #include <stdio.h> int main(int argc, char *argv[]) { char c[] = "china"; char *p = c; while (p) { printf("%c\n", *p++); } return 0; }
4. ⭐字符串操作函数
4.1 用gets和puts对一个字符串进行输入输出
gets[字符数组名]; puts[字符数组名或字符串];
eg:
char ch[10];
gets(ch); // 在键盘上输入一个字符串,保存到ch指向的内存空间中
puts(ch); // 把ch指向的空间中保存的字符串打印出来
注意:
a) gets有巨大的Bug,gets这个函数不会考虑保存这个字符串的数组的空间大小,
如果超过了分配范围还会继续写数据,可能会改变别的变量的数据#include <stdio.h> int main() { char c[3]; char b[3] = {'a','b','c'}; gets(c); // sscc // ssc puts(c); // sscc // ssc puts(b); // c // 空白并换行 return 0; } // 原因:终端输入的是一个字符串,末尾自动加了一个'\0' // 字符数组按内存从小到大连续存储(节约内存)
b) gets函数不会去获取 '\n',puts在输出时将字符串结束标志'\0'转换成'\n',即自动 换行
c) gets获取字符串时,可以获取空格、tab等空白字符,回车结束输入
而 scanf 是遇到空格、tab、回车等空白字符就结束输入
终端指令:man gets // 查找man手册中 gets 函数的说明NAME gets - get a string from standard input (DEPRECATED) SYNOPSIS #include <stdio.h> char* gets(char *s);
4.2 strcpy / strncpy:字符串拷贝函数
strcpy(字符数组名1,字符串或字符数组名2)
不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组,字符数组名是一个地址常量,不能改变其值
NAME strcpy, strncpy - copy a string SYNOPSIS #include <string.h> strcpy:字符串拷贝函数.用来把src指向的字符串,拷贝到dest指向的内存中去 char* strcpy(char *dest, const char *src); @dest:指向用来保存字符串的内存空间 @src:指向要拷贝的字符串数据 返回值: 如果成功,返回拷贝后字符串的首地址, 即 dest 如果失败,返回 NULL 注意:strcpy函数也有一个Bug,不关心dest指向的空间是否够用,造成内存溢出
strncpy是为了修复strcpy的bug的,它的功能和strcpy相似, 只不过他顶多拷贝n个字节 char* strncpy(char *dest, const char *src, size_t n); @dest:指向用来保存字符串的内存空间 @src:指向要拷贝的字符串数据 @n:最多拷贝n个字节 返回值: 如果成功,返回拷贝后字符串的首地址, 即 dest 如果失败,返回 NULL
注意:strncpy到底拷贝了多少个字节呢?
1) 遇到 '\0',拷贝结束('\0'有拷贝的)
2) 没遇到'\0',但是已经拷了n个字节了,结束拷贝(没有拷贝'\0')如果在复制前未对字符数组1初始化或赋值,则字符数组1各字节中的内容是无法预知的,复制时将字符数组2中的字符串和其后的'\0'一起复制到字符数组1中,取代字符数组1中的前面几个字符,最后几个字符并不一定是'\0',而是字符数组1中的最后几个字节的内容
字符数组按内存从小到大连续存储(节约内存) char ch1[] = {'a', 'b', 'c'}; char ch2[] = {'b', 'c', 'd'}; // 存储 a b c b c d // 从小到大排序 a b c a a a b c \0 \0 ...... char ch1[100] = {'a', 'b', 'c'}; char ch2[] = {'a', 'b', 'c', 'a', 'a'}; puts(ch2); // abcaaabc strcpy(ch1, ch2); printf("%s", ch1); // abcaaabc // 从小到大排序 a b c a a \0 \0 \0 \0 \0 a b c \0 \0 ...... char ch1[100] = {'a', 'b', 'c'}; char ch2[10] = {'a', 'b', 'c', 'a', 'a'}; puts(ch2); // abcaa 原因:有'\0' strcpy(ch1, ch2); printf("%s", ch1); // abcaa
注意事项:
1. 遇到 '\0'拷贝结束('\0'有拷贝的)
2. 字符数组2的大小如果小于字符数组1的大小:必须以 '\0'结束
#include <stdio.h> #include <string.h> // 错误 数组arr1越界,'\0'是停止拷贝的终止条件 int main(int argc, char *argv[]) { // 字符数组按内存从小到大连续存储(节约内存) abcd********** char arr1[] = "**********"; char arr2[] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); return 0; } // 正确 int main(int argc, char *argv[]) { // 字符数组按内存从小到大连续存储(节约内存) abcd**********\0 char arr1[14] = "**********"; // 10个‘*’ 有'\0'结尾 char arr2[] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); // 输出:abcd********** return 0; } // 正确 int main(int argc, char *argv[]) { // 字符数组按内存从小到大连续存储(节约内存) abcd\0********** char arr1[10] = "**********"; char arr2[] = { 'a', 'b', 'c', 'd', '\0'}; // 有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); // 输出abcd return 0; } // 正确 int main(int argc, char *argv[]) { char arr1[4] = "****"; char arr2[4] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); // 输出:abcd return 0; } // 错误 int main(int argc, char *argv[]) { char arr1[5] = "****"; // 有'\0'结尾 char arr2[4] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); return 0; }
3. 目标空间必须足够大,以确保能放源字符串
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { // 字符数组按内存从小到大连续存储(节约内存) char arr1[6] = "*****"; char arr2[] = "hello world"; printf("%s\n", strcpy(arr1, arr2)); printf("%s\n", arr2); return 0; } hello world world
这里虽然拷贝成功并将结果输出了,但程序却崩溃了。目标空间太小,不足以放置拷贝的源字符串,会造成溢出的情况,从而改变字符arr2中的值
4. 目标空间必须可变
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *str1 = "hello world"; char str2[10] = "*********"; printf("%s\n", strcpy(str1, str2)); return 0; }
这里的程序也出现了错误。str1指向的是常量字符串,是不可以被修改掉的,目标空间必须是可以被修改的,因为要将拷贝的字符串放在目标空间中。而源字符串可以是能够修改的、也可以是不能修改的,因为strcpy函数的第二个参数已经用const关键字修饰了,保证了拷贝过程中不会被修改
5. arr1如果是指针,必须要有指向,且指向的内容必须可改变
#include <stdio.h> #include <string.h> #include <stdlib.h> // 正确 int main(int argc, char *argv[]) { char str[10] = "aa"; char *arr1 = str; // 字符数组按内存从小到大连续存储(节约内存) arr1以str为依据 char arr2[] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); // 输出:abcdaa return 0; } // 错误 int main(int argc, char *argv[]) { char *arr1; // 或 char *arr1 = NULL; // 野指针 char arr2[5] = {'a', 'b', 'c', 'd'}; // 有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); return 0; } // 正确 int main(int argc, char *argv[]) { char *arr1 = malloc(sizeof(4)); char arr2[] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾 printf("%s\n", strcpy(arr1, arr2)); // 输出:abcd return 0; } // 正确 int main(int argc, char *argv[]) { const char *s = "hello"; // s指向6个字节大小的字符串 char *p = malloc(strlen(s)); // 分配5个字节 printf("%s\n", strcpy(p, s)); // hello printf("%ld\n", sizeof("hello")); // 6 return 0; }
4.3 strlen
求s指向的字符串的长度,并返回字符串长度(是指字符串里面包含的字符个数(不算后面'\0'))遇到'\0'才结束 %ld
strlen(字符串或字符数组) 字符'\0'的ASCII值是0,字符'0'的ASCII值是48
NAME strlen - calculate the length of a string SYNOPSIS #include <string.h> size_t strlen(const char *s);
char ch1[5] = {'a', 'b', 'c'}; char ch2[3] = {'a', 'b', 'c'}; printf("%ld", strlen(ch1)); // 3 printf("%ld", strlen(ch2)); // 6
char ch1[] = {'a', 'b', 'c'}; char ch2[3] = {'a', 'b', 'c'}; printf("%ld", strlen(ch1)); // 6
char ch[] = {'a', 'b', 'c'}; strlen(ch); // 由于str数组没有字符串结束符,长度不能确定
char ch[3] = {'a', 'b', 'c'}; strlen(ch); // 3
4.4 strcat / strncat:字符串连接函数
strcat(字符数组名1,字符数组名2或字符串)
NAME strcat, strncat - concatenate two strings SYNOPSIS #include <string.h> char* strcat(char *dest, const char *src); strcat:用来把src指向的字符串拷贝到dest指向的字符串的末尾(覆盖掉'\0') @dest:指向用来保存字符串的内存空间,注意空间大小要足够 @src:指向要拷贝的字符串数据 返回值: 如果成功,返回拷贝后字符串的首地址,即 dest 如果失败,返回 NULL
注意:strcat函数也有一个Bug,不关心dest指向的空间是否够用,造成内存溢出
strncat正是为了修复strcat的bug的,它的功能和strcat相似char* strncat(char *dest, const char *src, size_t n); @dest:指向用来保存字符串的内存空间 @src:指向要拷贝的字符串数据 @n:最多拷贝n个字节 返回值: 如果成功,返回拷贝后字符串的首地址, 即 dest 如果失败,返回 NULL
#include <stdio.h> #include <string.h> int main () { char src[50], dest[50]; strcpy(src, "This is source"); strcpy(dest, "This is destination"); strcat(dest, src); printf("最终的目标字符串: |%s|", dest); return(0); } // 最终的目标字符串: |This is destinationThis is source|
4.5 strcmp / strncmp:字符串比较函数
strcmp(字符串1,字符串2)
那么字符串怎么比较呢?
都是从第一个字符开始比较,一个字符接着一个字符比较ASCII码值的大小
if c1 > c2,则返回 正数
if c1 < c2,则返回 负数
if c1 == c2,则继续比较下一个,如果全部相等才返回 01)在英文字典中位置在后面的ASCII值“大”
2)小写字母比大写字母的ASCII值“大”
NAME strcmp, strncmp - compare two strings SYNOPSIS #include <string.h> int strcmp(const char *s1, const char *s2); 注意:strcmp 无bug,因为没有进行写操作 strncmp:功能和strcmp相似,只不过比较前n个字符 int strncmp(const char *s1, const char *s2, size_t n);
4.6 大小写转换
#include <string.h> strlwr函数-----转换为小写的函数 strlwr(字符串) 将字符串中大写字母换成小写字母 -------------------------------------------------------------------- strupr函数-----转换为大写的函数 strupr(字符串) 将字符串中大写字母换成小写字母
4.7 memset:初始化函数
memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值
#include <string.h> void* memset(void *s, int c, size_t n); s指向要填充的内存块 c是要被设置的值 n是要被设置该值的字符数 返回类型是一个指向存储区s的指针
// 1) int arr[m][n]; memset(arr, 0, sizeof(int) * m * n); // 全部初始化为0 // 2) int **dp = (int**)malloc(sizeof(int*) * m); for (int i = 0; i < m; i++) { dp[i] = (int*)malloc(sizeof(int) * n); memset(dp[i], 0, sizeof(dp[i])); }
需要说明的几个地方
一、不能任意赋值
memset函数是按照字节对内存块进行初始化,所以不能用它将int数组去初始化为0和-1之外的其他值(除非该值高字节和低字节相同),可以给bool类型赋值(false、true)
其实c的实际范围应该在0~255,因为memset函数只能取c的后八位给所输入范围的每个字节。也就是说无论c多大只有后八位二进制是有效的=================================================================================================
对于int a[4];
memset(a, -1, sizeof(a)) 与 memset(a, 511, sizeof(a)) 所赋值的结果一样都为-1:
因为 -1 的二进制码(补码)为(11111111 11111111 11111111 11111111);511 的二进制码为(00000000 00000000 00000001 11111111);
后八位均为(11111111),所以数组中的每个字节都被赋值为(11111111)。
注意int占四个字节,例如a[0]的四个字节都被赋值为(11111111),那么a[0](11111111 11111111 11111111 11111111),即a[0] = -1二、注意所要赋值的数组的元素类型
#include <stdio.h> #include <string.h> // 例一:对char类型的数组a初始化,设置元素全为’1’ int main(int argc, char *argv[]) { char a[4]; memset(a, '1', 4); for(int i = 0; i < 4; i++) { printf("%c ", a[i]); // 输出:1 1 1 1 } putchar('\n'); return 0; } // 例二:对int类型的数组a初始化,设置元素值全为1 int main(int argc, char *argv[]) { int a[4]; memset(a, 1, sizeof(a)); // 或者memset(a, 1, 16) 0000 0001 0000 0001 0000 0001 0000 0001 for(int i = 0; i < 4; i++) { printf("%d ", a[i]); // 输出:16843009 16843009 16843009 16843009 } putchar('\n'); return 0; } int main(int argc, char *argv[]) { int a[4]; memset(a, 1, 4); // 0000 0001 0000 0001 0000 0001 0000 0001 for(int i = 0; i < 4; i++) { printf("%d ", a[i]); // 输出:16843009 0 0 0 } putchar('\n'); return 0; }
例一程序中,数组a是字符型的,字符型占据的内存大小就是1Byte,而memset函数也是以字节为单位进行赋值的,所以输出正确
例二程序中,数组a是整型的,整型占据的内存大小为4Byte,而memset函数还是按照字节为单位进行赋值,将1(0000 0001)赋给每一个字节。那么对于a[0]来说,其值为(00000001 00000001 00000001 00000001),即十进制的16843009注意:
#include <stdio.h> #include <string.h> /* 错误 当数组作为参数传递时,其传递的实际上是一个指针,这个指针指向数组的首地址, 如果用sizeof(a)函数得到的只是指针的长度,而不是数组的长度。 */ void fun1(int a[]) { memset(a, -1, sizeof(a)); // sizeof(a)是指针长度 } int main(int argc, char *argv[]) { int a[6]; fun1(a); for(int i = 0; i < 6; i++) { printf("%d ", a[i]); } printf("\n"); return 0; } // 正确 void fun1(int a[], int len) { memset(a, -1, len); } int main(int argc, char *argv[]) { int a[6]; int len = sizeof(a); fun1(a, len); for(int i = 0; i < 6; i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
4.8 strdup:字符串拷贝
一. 函数分析
1. 函数原型:
#include <string.h> char* strdup(const char *s);
2. 功能:
strdup()函数主要是拷贝字符串s的一个副本,由函数返回值返回,这个副本有自己的内存空间,和s没有关联。strdup函数复制一个字符串,使用完后,要使用delete函数删除在函数中动态申请的内存,strdup函数的参数不能为NULL,一旦为NULL,就会报段错误,因为该函数包括了strlen函数,而该函数参数不能是NULL
3. strdup函数实现
char* __strdup(const char *s) { size_t len = strlen(s) +1; void *new = malloc(len); if (new == NULL) { return NULL; } return (char*)memcpy(new, s, len); }
4. 函数实例
#include <stdio.h> #include <string.h> int main(void) { char *src = "This is the jibo"; char *dest; dest = strdup(src); printf(“the dest %s\n”, dest); free(dest); return 0; }
1. 共同点:
两个函数都实现了字符串的拷贝。
2. 不同点:
1) strcpy函数:把从src地址开始且含有结束符的字符串复制到以dest开始的地址空间
2)由strcpy和strdup函数实现可知
strdup函数返回指向被复制的字符串的指针,所需空间由malloc()函数分配且可以由free()函数释放。stdrup可以直接把要复制的内容复制给没有初始化的指针,因为它会自动分配空间给目的指针
strcpy的目的指针一定是已经分配好的内存指针。
3)strdup的缺点:
使用strdup函数的时候,往往会忘记内存的释放,因为申请内存空间的动作是在strdup函数内实现,如果对该函数的实现不是很了解,则会忘记使用free函数来释放空间