首页
前端面试题
前端报错总结
电子书
更多
插件下载
Search
1
JavaScript基础(二)操作符 流程控制
42 阅读
2
HTML基础
20 阅读
3
Vue基础
17 阅读
4
wctype.h
14 阅读
5
Vue2(知识点)
13 阅读
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
登录
Search
liuxiaobai
累计撰写
108
篇文章
累计收到
12
条评论
首页
栏目
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
104
篇与
的结果
2023-09-20
字符串
字符串简介C 语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}处理的。编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C 语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的 ASCII 码是0(二进制形式00000000),后者的 ASCII 码是48(二进制形式00110000)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}。所有字符串的最后一个字符,都是\0。这样做的好处是,C 语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是\0,那么就知道字符串结束了。char localString[10];上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。字符串写成数组的形式,是非常麻烦的。C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。{'H', 'e', 'l', 'l', 'o', '\0'} // 等价于 "Hello"上面两种字符串的写法是等价的,内部存储方式都是一样的。双引号里面的字符串,不用自己添加结尾字符\0,C 语言会自动添加。注意,双引号里面是字符串,单引号里面是字符,两者不能互换。如果把Hello放在单引号里面,编译器会报错。// 报错 'Hello'另一方面,即使双引号里面只有一个字符(比如"a"),也依然被处理成字符串(存储为2个字节),而不是字符'a'(存储为1个字节)。如果字符串内部包含双引号,则该双引号需要使用反斜杠转义。"She replied, \"It does.\""反斜杠还可以表示其他特殊字符,比如换行符(\n)、制表符(\t)等。"Hello, world!\n"如果字符串过长,可以在需要折行的地方,使用反斜杠(\)结尾,将一行拆成多行。"hello \ world"上面示例中,第一行尾部的反斜杠,将字符串拆成两行。上面这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。为了解决这个问题,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[14] = "Hello, world!"; // 写法二 char* s = "Hello, world!";上面两种写法都声明了一个字符串变量s。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。char s[] = "Hello, world!";上面示例中,编译器会将数组s的长度指定为14,正好容纳后面的字符串。字符数组的长度,可以大于字符串的实际长度。char s[50] = "hello";上面示例中,字符数组s的长度是50,但是字符串“hello”的实际长度只有6(包含结尾符号\0),所以后面空出来的44个位置,都会被初始化为\0。字符数组的长度,不能小于字符串的实际长度。char s[5] = "hello";上面示例中,字符串数组s的长度是5,小于字符串“hello”的实际长度6,这时编译器会报错。因为如果只将前5个字符写入,而省略最后的结尾符号\0,这很可能导致后面的字符串相关代码出错。字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。char* s = "Hello, world!"; s[0] = 'z'; // 错误上面代码使用指针,声明了一个字符串变量,然后修改了字符串的第一个字符。这种写法是错的,会导致难以预测的后果,执行时很可能会报错。如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。char s[] = "Hello, world!"; s[0] = 'z';为什么字符串声明为指针时不能修改,声明为数组时就可以修改?原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const说明符,保证该字符串是只读的。const char* s = "Hello, world!";上面字符串声明为指针时,使用了const说明符,就保证了该字符串无法修改。一旦修改,编译器肯定会报错。第二个差异是,指针变量可以指向其它字符串。char* s = "hello"; s = "world";上面示例中,字符指针可以指向另一个字符串。但是,字符数组变量不能指向另一个字符串。char s[] = "hello"; s = "world"; // 报错上面示例中,字符数组的数组名,总是指向初始化时的字符串地址,不能修改。同样的原因,声明字符数组后,不能直接用字符串赋值。char s[10]; s = "abc"; // 错误上面示例中,不能直接把字符串赋值给字符数组变量,会报错。原因是字符数组的变量名,跟所指向的数组是绑定的,不能指向另一个地址。为什么数组变量不能赋值为另一个数组?原因是数组变量所在的地址无法改变,或者说,编译器一旦为数组变量分配地址后,这个地址就绑定这个数组变量了,这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。想要重新赋值,必须使用 C 语言原生提供的strcpy()函数,通过字符串拷贝完成赋值。这样做以后,数组变量的地址还是不变的,即strcpy()只是在原地址写入新的字符串,而不是让数组变量指向新的地址。char s[10]; strcpy(s, "abc");上面示例中,strcpy()函数把字符串abc拷贝给变量s,这个函数的详细用法会在后面介绍。strlen()strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。// string.h size_t strlen(const char* s);它的参数是字符串变量,返回的是size_t类型的无符号整数,除非是极长的字符串,一般情况下当作int类型处理即可。下面是一个用法实例。char* str = "hello"; int len = strlen(str); // 5strlen()的原型在标准库的string.h文件中定义,使用时需要加载头文件string.h。#include <stdio.h> #include <string.h> int main(void) { char* s = "Hello, world!"; printf("The string is %zd characters long.\n", strlen(s)); }注意,字符串长度(strlen())与字符串变量长度(sizeof()),是两个不同的概念。char s[50] = "hello"; printf("%d\n", strlen(s)); // 5 printf("%d\n", sizeof(s)); // 50上面示例中,字符串长度是5,字符串变量长度是50。如果不使用这个函数,可以通过判断字符串末尾的\0,自己计算字符串长度。int my_strlen(char *s) { int count = 0; while (s[count] != '\0') count++; return count; }strcpy()字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。char str1[10]; char str2[10]; str1 = "abc"; // 报错 str2 = str1; // 报错上面两种字符串的复制写法,都是错的。因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。如果是字符指针,赋值运算符(=)只是将一个指针的地址复制给另一个指针,而不是复制字符串。char* s1; char* s2; s1 = "abc"; s2 = s1;上面代码可以运行,结果是两个指针变量s1和s2指向同一字符串,而不是将字符串s1的内容复制给s2。C 语言提供了strcpy()函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。strcpy(char dest[], const char source[])strcpy()接受两个参数,第一个参数是目的字符串数组,第二个参数是源字符串数组。复制字符串之前,必须要保证第一个参数的长度不小于第二个参数,否则虽然不会报错,但会溢出第一个字符串变量的边界,发生难以预料的结果。第二个参数的const说明符,表示这个函数不会修改第二个字符串。#include <stdio.h> #include <string.h> int main(void) { char s[] = "Hello, world!"; char t[100]; strcpy(t, s); t[0] = 'z'; printf("%s\n", s); // "Hello, world!" printf("%s\n", t); // "zello, world!" }上面示例将变量s的值,拷贝一份放到变量t,变成两个不同的字符串,修改一个不会影响到另一个。另外,变量t的长度大于s,复制后多余的位置(结束标志\0后面的位置)都为随机值。strcpy()也可以用于字符数组的赋值。char str[10]; strcpy(str, "abcd");上面示例将字符数组变量,赋值为字符串“abcd”。strcpy()的返回值是一个字符串指针(即char*),指向第一个参数。char* s1 = "beast"; char s2[40] = "Be the best that you can be."; char* ps; ps = strcpy(s2 + 7, s1); puts(s2); // Be the beast puts(ps); // beast上面示例中,从s2的第7个位置开始拷贝字符串beast,前面的位置不变。这导致s2后面的内容都被截去了,因为会连beast结尾的空字符一起拷贝。strcpy()返回的是一个指针,指向拷贝开始的位置。strcpy()返回值的另一个用途,是连续为多个字符数组赋值。strcpy(str1, strcpy(str2, "abcd"));上面示例调用两次strcpy(),完成两个字符串变量的赋值。另外,strcpy()的第一个参数最好是一个已经声明的数组,而不是声明后没有进行初始化的字符指针。char* str; strcpy(str, "hello world"); // 错误上面的代码是有问题的。strcpy()将字符串分配给指针变量str,但是str并没有进行初始化,指向的是一个随机的位置,因此字符串可能被复制到任意地方。如果不用strcpy(),自己实现字符串的拷贝,可以用下面的代码。char* strcpy(char* dest, const char* source) { char* ptr = dest; while (*dest++ = *source++); return ptr; } int main(void) { char str[25]; strcpy(str, "hello world"); printf("%s\n", str); return 0; }上面代码中,关键的一行是while (*dest++ = *source++),这是一个循环,依次将source的每个字符赋值给dest,然后移向下一个位置,直到遇到\0,循环判断条件不再为真,从而跳出循环。其中,*dest++这个表达式等同于*(dest++),即先返回dest这个地址,再进行自增运算移向下一个位置,而*dest可以对当前位置赋值。strcpy()函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用strncpy()函数代替。strncpy()strncpy()跟strcpy()的用法完全一样,只是多了第3个参数,用来指定复制的最大字符数,防止溢出目标字符串变量的边界。char* strncpy( char* dest, char* src, size_t n );上面原型中,第三个参数n定义了复制的最大字符数。如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符\0,这一点务必注意。如果源字符串的字符数小于n,则strncpy()的行为与strcpy()完全一致。strncpy(str1, str2, sizeof(str1) - 1); str1[sizeof(str1) - 1] = '\0';上面示例中,字符串str2复制给str1,但是复制长度最多为str1的长度减去1,str1剩下的最后一位用于写入字符串的结尾标志\0。这是因为strncpy()不会自己添加\0,如果复制的字符串片段不包含结尾标志,就需要手动添加。strncpy()也可以用来拷贝部分字符串。char s1[40]; char s2[12] = "hello world"; strncpy(s1, s2, 5); s1[5] = '\0'; printf("%s\n", s1); // hello上面示例中,指定只拷贝前5个字符。strcat()strcat()函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。该函数的原型定义在string.h头文件里面。char* strcat(char* s1, const char* s2);strcat()的返回值是一个字符串指针,指向第一个参数。char s1[12] = "hello"; char s2[6] = "world"; strcat(s1, s2); puts(s1); // "helloworld"上面示例中,调用strcat()以后,可以看到字符串s1的值变了。注意,strcat()的第一个参数的长度,必须足以容纳添加第二个参数字符串。否则,拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的strncat()代替。strncat()strncat()用于连接两个字符串,用法与strcat()完全一致,只是增加了第三个参数,指定最大添加的字符数。在添加过程中,一旦达到指定的字符数,或者在源字符串中遇到空字符\0,就不再添加了。它的原型定义在string.h头文件里面。char* strncat( const char* dest, const char* src, size_t n );strncat()返回第一个参数,即目标字符串指针。为了保证连接后的字符串,不超过目标字符串的长度,strncat()通常会写成下面这样。strncat( str1, str2, sizeof(str1) - strlen(str1) - 1 );strncat()总是会在拼接结果的结尾,自动添加空字符\0,所以第三个参数的最大值,应该是str1的变量长度减去str1的字符串长度,再减去1。下面是一个用法实例。char s1[10] = "Monday"; char s2[8] = "Tuesday"; strncat(s1, s2, 3); puts(s1); // "MondayTue"上面示例中,s1的变量长度是10,字符长度是6,两者相减后再减去1,得到3,表明s1最多可以再添加三个字符,所以得到的结果是MondayTue。strcmp()如果要比较两个字符串,无法直接比较,只能一个个字符进行比较,C 语言提供了strcmp()函数。strcmp()函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h头文件里面。int strcmp(const char* s1, const char* s2);按照字典顺序,如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。下面是一个用法示例。// s1 = Happy New Year // s2 = Happy New Year // s3 = Happy Holidays strcmp(s1, s2) // 0 strcmp(s1, s3) // 大于 0 strcmp(s3, s1) // 小于 0注意,strcmp()只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==)就能比较。所以,不要把字符类型(char)的值,放入strcmp()当作参数。strncmp()由于strcmp()比较的是整个字符串,C 语言又提供了strncmp()函数,只比较到指定的位置。该函数增加了第三个参数,指定了比较的字符数。它的原型定义在string.h头文件里面。int strncmp( const char* s1, const char* s2, size_t n );它的返回值与strcmp()一样。如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。下面是一个例子。char s1[12] = "hello world"; char s2[12] = "hello C"; if (strncmp(s1, s2, 5) == 0) { printf("They all have hello.\n"); }上面示例只比较两个字符串的前5个字符。sprintf(),snprintf()sprintf()函数跟printf()类似,但是用于将数据写入字符串,而不是输出到显示器。该函数的原型定义在stdio.h头文件里面。int sprintf(char* s, const char* format, ...);sprintf()的第一个参数是字符串指针变量,其余参数和printf()相同,即第二个参数是格式字符串,后面的参数是待写入的变量列表。char first[6] = "hello"; char last[6] = "world"; char s[40]; sprintf(s, "%s %s", first, last); printf("%s\n", s); // hello world上面示例中,sprintf()将输出内容组合成“hello world”,然后放入了变量s。sprintf()的返回值是写入变量的字符数量(不计入尾部的空字符\0)。如果遇到错误,返回负值。sprintf()有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,sprintf()依然会将其写入,导致发生溢出。为了控制写入的字符串的长度,C 语言又提供了另一个函数snprintf()。snprintf()只比sprintf()多了一个参数n,用来控制写入变量的字符串不超过n - 1个字符,剩下一个位置写入空字符\0。下面是它的原型。int snprintf(char*s, size_t n, const char* format, ...);snprintf()总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数,snprintf()会写入 n - 1 个字符,留出最后一个位置写入空字符。下面是一个例子。snprintf(s, 12, "%s %s", "hello", "world");上面的例子中,snprintf()的第二个参数是12,表示写入字符串的最大长度不超过12(包括尾部的空字符)。snprintf()的返回值是写入格式字符串的字符数量(不计入尾部的空字符\0)。如果n足够大,返回值应该小于n,但是有时候格式字符串的长度可能大于n,那么这时返回值会大于n,但实际上真正写入变量的还是n-1个字符。如果遇到错误,返回一个负值。因此,返回值只有在非负并且小于n时,才能确认完整的格式字符串写入了变量。字符串数组如果一个数组的每个成员都是一个字符串,需要通过二维的字符数组实现。每个字符串本身是一个字符数组,多个字符串再组成一个数组。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]); }
2023年09月20日
1 阅读
0 评论
0 点赞
2023-09-20
数组
数组简介数组是一组相同类型的值,按照顺序储存在一起。数组通过变量名后加方括号表示,方括号里面是数组的成员数量。int scores[100];上面示例声明了一个数组scores,里面包含100个成员,每个成员都是int类型。注意,声明数组时,必须给出数组的大小。数组的成员从0开始编号,所以数组scores[100]就是从第0号成员一直到第99号成员,最后一个成员的编号会比数组长度小1。数组名后面使用方括号指定编号,就可以引用该成员。也可以通过该方式,对该位置进行赋值。scores[0] = 13; scores[99] = 42;上面示例对数组scores的第一个位置和最后一个位置,进行了赋值。注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,所以必须非常小心。int scores[100]; scores[100] = 51;上面示例中,数组scores只有100个成员,因此scores[100]这个位置是不存在的。但是,引用这个位置并不会报错,会正常运行,使得紧跟在scores后面的那块内存区域被赋值,而那实际上是其他变量的区域,因此不知不觉就更改了其他变量的值。这很容易引发错误,而且难以发现。数组也可以在声明时,使用大括号,同时对每一个成员赋值。int a[5] = {22, 37, 3490, 18, 95};注意,使用大括号赋值时,必须在数组声明时赋值,否则编译时会报错。int a[5]; a = {22, 37, 3490, 18, 95}; // 报错上面代码中,数组a声明之后再进行大括号赋值,导致报错。报错的原因是,C 语言规定,数组变量一旦声明,就不得修改变量指向的地址,具体会在后文解释。由于同样的原因,数组赋值之后,再用大括号修改值,也是不允许的。int a[5] = {1, 2, 3, 4, 5}; a = {22, 37, 3490, 18, 95}; // 报错上面代码中,数组a赋值后,再用大括号重新赋值也是不允许的。使用大括号赋值时,大括号里面的值不能多于数组的长度,否则编译时会报错。如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为0。int a[5] = {22, 37, 3490}; // 等同于 int a[5] = {22, 37, 3490, 0, 0};如果要将整个数组的每一个成员都设置为零,最简单的写法就是下面这样。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。int a[] = {[2] = 6, [9] = 12};上面示例中,数组a的最大指定位置是9,所以数组的长度是10。数组长度sizeof运算符会返回整个数组的字节长度。int a[] = {22, 37, 3490}; int arrLen = sizeof(a); // 12上面示例中,sizeof返回数组a的字节长度是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。多维数组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};变长数组数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)。int n = x + y; int arr[n];上面示例中,数组arr就是变长数组,因为它的长度取决于变量n的值,编译器没法事先确定,只有运行时才能知道n是多少。变长数组的根本特征,就是数组长度只有运行时才能确定。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。任何长度需要运行时才能确定的数组,都是变长数组。int i = 10; int a1[i]; int a2[i + 5]; int a3[i + k];上面示例中,三个数组的长度都需要运行代码才能知道,编译器并不知道它们的长度,所以它们都是变长数组。变长数组也可以用于多维数组。int m = 4; int n = 5; int c[m][n];上面示例中,c[m][n]就是二维变长数组。数组的地址数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。请看下面的例子。int a[5] = {11, 22, 33, 44, 55}; int* p; p = &a[0]; printf("%d\n", *p); // Prints "11"上面示例中,&a[0]就是数组a的首个成员11的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p),可以获得首个成员的值11。由于数组的起始地址是常用操作,&array[0]的写法有点麻烦,C 语言提供了便利写法,数组名等同于起始地址,也就是说,数组名就是指向第一个成员(array[0])的指针。int a[5] = {11, 22, 33, 44, 55}; int* p = &a[0]; // 等同于 int* p = a;上面示例中,&a[0]和数组名a是等价的。这样的话,如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。函数接受数组作为参数,函数原型可以写成下面这样。// 写法一 int sum(int arr[], int len); // 写法二 int sum(int* arr, int len);上面示例中,传入一个整数数组,与传入一个整数指针是同一回事,数组符号[]与指针符号*是可以互换的。下一个例子是通过数组指针对成员求和。int sum(int* arr, int len) { int i; int total = 0; // 假定数组有 10 个成员 for (i = 0; i < len; i++) { total += arr[i]; } return total; }上面示例中,传入函数的是一个指针arr(也是数组名)和数组长度,通过指针获取数组的每个成员,从而求和。*和&运算符也可以用于多维数组。int a[4][2]; // 取出 a[0][0] 的值 *(a[0]); // 等同于 **a上面示例中,由于a[0]本身是一个指针,指向第二维数组的第一个成员a[0][0]。所以,*(a[0])取出的是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; // 报错上面两种写法都会更改数组b的地址,导致报错。数组指针的加减法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 + i的每轮循环每次都会指向下一个成员的地址,*(a + i)取出该地址的值,等同于a[i]。对于数组的第一个成员,*(a + 0)(即*a)等同于a[0]。由于数组名与指针是等价的,所以下面的等式总是成立。a[b] == *(a + b)上面代码给出了数组成员的两种访问方式,一种是使用方括号a[b],另一种是使用指针*(a + b)。如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组。int a[] = {11, 22, 33, 44, 55, 999}; int* p = a; while (*p != 999) { printf("%d\n", *p); p++; }上面示例中,通过p++让变量p指向下一个成员。注意,数组名指向的地址是不能变的,所以上例中,不能直接对a进行自增,即a++的写法是错的,必须将a的地址赋值给指针变量p,然后对p进行自增。遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现。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 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]。同一个数组的两个成员的指针相减时,返回它们之间的距离。int* p = &a[5]; int* q = &a[1]; printf("%d\n", p - q); // 4 printf("%d\n", q - p); // -4上面示例中,变量p和q分别是数组5号位置和1号位置的指针,它们相减等于4或-4。数组的复制由于数组名是指针,所以复制数组不能简单地复制数组名。int* a; int b[3] = {1, 2, 3}; a = b;上面的写法,结果不是将数组b复制给数组a,而是让a和b指向同一个数组。复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制。for (i = 0; i < N; i++) a[i] = b[i];上面示例中,通过将数组b的成员逐个复制给数组a,从而实现数组的赋值。另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份。memcpy(a, b, sizeof(b));上面示例中,将数组b所在的那段内存,复制给数组a。这种方法要比循环复制数组成员要快。作为函数的参数声明参数数组数组作为函数的参数,一般会同时传入数组名和数组长度。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)。变长数组作为参数变长数组作为函数参数时,写法略有不同。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()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。数组字面量作为参数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 [])类似于强制的类型转换,告诉编译器怎么理解这组值。
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
函数
函数简介函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。下面的例子就是一个函数。int plus_one(int n) { return n + 1; }上面的代码声明了一个函数plus_one()。函数声明的语法有以下几点,需要注意。(1)返回值类型。函数声明时,首先需要给出返回值的类型,上例是int,表示函数plus_one()返回一个整数。(2)参数。函数名后面的圆括号里面,需要声明参数的类型和参数名,plus_one(int n)表示这个函数有一个整数参数n。(3)函数体。函数体要写在大括号里面,后面(即大括号外面)不需要加分号。大括号的起始位置,可以跟函数名在同一行,也可以另起一行,本书采用同一行的写法。(4)return语句。return语句给出函数的返回值,程序运行到这一行,就会跳出函数体,结束函数的调用。如果函数没有返回值,可以省略return语句,或者写成return;。调用函数时,只要在函数名后面加上圆括号就可以了,实际的参数放在圆括号里面,就像下面这样。int a = plus_one(13); // a 等于 14函数调用时,参数个数必须与定义里面的参数个数一致,参数过多或过少都会报错。int plus_one(int n) { return n + 1; } plus_one(2, 2); // 报错 plus_one(); // 报错上面示例中,函数plus_one()只能接受一个参数,传入两个参数或不传参数,都会报错。函数必须声明后使用,否则会报错。也就是说,一定要在使用plus_one()之前,声明这个函数。如果像下面这样写,编译时会报错。int a = plus_one(13); int plus_one(int n) { return n + 1; }上面示例中,在调用plus_one()之后,才声明这个函数,编译就会报错。C 语言标准规定,函数只能声明在源码文件的顶层,不能声明在其他函数内部。不返回值的函数,使用void关键字表示返回值的类型。没有参数的函数,声明时要用void关键字表示参数类型。void myFunc(void) { // ... }上面的myFunc()函数,既没有返回值,调用时也不需要参数。函数可以调用自身,这就叫做递归(recursion)。下面是斐波那契数列的例子。unsigned long Fibonacci(unsigned n) { if (n > 2) return Fibonacci(n - 1) + Fibonacci(n - 2); else return 1; }上面示例中,函数Fibonacci()调用了自身,大大简化了算法。main()C 语言规定,main()是程序的入口函数,即所有的程序一定要包含一个main()函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。其他函数都是通过它引入程序的。main()的写法与其他函数一样,要给出返回值的类型和参数的类型,就像下面这样。int main(void) { printf("Hello World\n"); return 0; }上面示例中,最后的return 0;表示函数结束运行,返回0。C 语言约定,返回值0表示函数运行成功,如果返回其他非零整数,就表示运行失败,代码出了问题。系统根据main()的返回值,作为整个程序的返回值,确定程序是否运行成功。正常情况下,如果main()里面省略return 0这一行,编译器会自动加上,即main()的默认返回值为0。所以,写成下面这样,效果完全一样。int main(void) { printf("Hello World\n"); }由于 C 语言只会对main()函数默认添加返回值,对其他函数不会这样做,所以建议总是保留return语句,以便形成统一的代码风格。参数的传值引用如果函数的参数是一个变量,那么调用时,传入的是这个变量的值的拷贝,而不是变量本身。void increment(int a) { a++; } int i = 10; increment(i); printf("%d\n", i); // 10上面示例中,调用increment(i)以后,变量i本身不会发生变化,还是等于10。因为传入函数的是i的拷贝,而不是i本身,拷贝的变化,影响不到原始变量。这就叫做“传值引用”。所以,如果参数变量发生变化,最好把它作为返回值传出来。int increment(int a) { a++; return a; } int i = 10; i = increment(i); printf("%d\n", i); // 11再看下面的例子,Swap()函数用来交换两个变量的值,由于传值引用,下面的写法不会生效。void Swap(int x, int y) { int temp; temp = x; x = y; y = temp; } int a = 1; int b = 2; Swap(a, b); // 无效上面的写法不会产生交换变量值的效果,因为传入的变量是原始变量a和b的拷贝,不管函数内部怎么操作,都影响不了原始变量。如果想要传入变量本身,只有一个办法,就是传入变量的地址。void Swap(int* x, int* y) { int temp; temp = *x; *x = *y; *y = temp; } int a = 1; int b = 2; Swap(&a, &b);上面示例中,通过传入变量x和y的地址,函数内部就可以直接操作该地址,从而实现交换两个变量的值。虽然跟传参无关,这里特别提一下,函数不要返回内部变量的指针。int* f(void) { int i; // ... return &i; }上面示例中,函数返回内部变量i的指针,这种写法是错的。因为当函数结束运行时,内部变量就消失了,这时指向内部变量i的内存地址就是无效的,再去使用这个地址是非常危险的。函数指针函数本身就是一段内存里面的代码,C 语言允许通过指针获取函数。void print(int a) { printf("%d\n", a); } void (*print_ptr)(int) = &print;上面示例中,变量print_ptr是一个函数指针,它指向函数print()的地址。函数print()的地址可以用&print获得。注意,(*print_ptr)一定要写在圆括号里面,否则函数参数(int)的优先级高于*,整个式子就会变成void* print_ptr(int)。有了函数指针,通过它也可以调用函数。(*print_ptr)(10); // 等同于 print(10);比较特殊的是,C 语言还规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说,print和&print是一回事。if (print == &print) // true因此,上面代码的print_ptr等同于print。void (*print_ptr)(int) = &print; // 或 void (*print_ptr)(int) = print; if (print_ptr == print) // true所以,对于任意函数,都有五种调用函数的写法。// 写法一 print(10) // 写法二 (*print)(10) // 写法三 (&print)(10) // 写法四 (*print_ptr)(10) // 写法五 print_ptr(10)为了简洁易读,一般情况下,函数名前面都不加*和&。这种特性的一个应用是,如果一个函数的参数或返回值,也是一个函数,那么函数原型可以写成下面这样。int compute(int (*myfunc)(int), int, int);上面示例可以清晰地表明,函数compute()的第一个参数也是一个函数。函数原型前面说过,函数必须先声明,后使用。由于程序总是先运行main()函数,导致所有其他函数都必须在main()函数之前声明。void func1(void) { } void func2(void) { } int main(void) { func1(); func2(); return 0; }上面代码中,main()函数必须在最后声明,否则编译时会产生警告,找不到func1()或func2()的声明。但是,main()是整个程序的入口,也是主要逻辑,放在最前面比较好。另一方面,对于函数较多的程序,保证每个函数的顺序正确,会变得很麻烦。C 语言提供的解决方法是,只要在程序开头处给出函数原型,函数就可以先使用、后声明。所谓函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。int twice(int); int main(int num) { return twice(num); } int twice(int num) { return 2 * num; }上面示例中,函数twice()的实现是放在main()后面,但是代码头部先给出了函数原型,所以可以正确编译。只要提前给出函数原型,函数具体的实现放在哪里,就不重要了。函数原型包括参数名也可以,虽然这样对于编译器是多余的,但是阅读代码的时候,可能有助于理解函数的意图。int twice(int); // 等同于 int twice(int num);上面示例中,twice函数的参数名num,无论是否出现在原型里面,都是可以的。注意,函数原型必须以分号结尾。一般来说,每个源码文件的头部,都会给出当前脚本使用的所有函数的原型。exit()exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。// 程序运行成功 // 等同于 exit(0); exit(EXIT_SUCCESS); // 程序异常中止 // 等同于 exit(1); exit(EXIT_FAILURE);在main()函数里面,exit()等价于使用return语句。其他函数使用exit(),就是终止整个程序的运行,没有其他作用。C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h。int atexit(void (*func)(void));atexit()的参数是一个函数指针。注意,它的参数函数(下例的print)不能接受参数,也不能有返回值。void print(void) { printf("something wrong!\n"); } atexit(print); exit(EXIT_FAILURE);上面示例中,exit()执行时会先自动调用atexit()注册的print()函数,然后再终止程序。函数说明符C 语言提供了一些函数说明符,让函数用法更加明确。extern 说明符对于多文件的项目,源码文件会用到其他文件声明的函数。这时,当前文件里面,需要给出外部函数的原型,并用extern说明该函数的定义来自其他文件。extern int foo(int arg1, char arg2); int main(void) { int a = foo(2, 3); // ... return 0; }上面示例中,函数foo()定义在其他文件,extern告诉编译器当前文件不包含该函数的定义。不过,由于函数原型默认就是extern,所以这里不加extern,效果是一样的。static 说明符默认情况下,每次调用函数时,函数的内部变量都会重新初始化,不会保留上一次运行的值。static说明符可以改变这种行为。static用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。#include <stdio.h> void counter(void) { static int count = 1; // 只初始化一次 printf("%d\n", count); count++; } int main(void) { counter(); // 1 counter(); // 2 counter(); // 3 counter(); // 4 }上面示例中,函数counter()的内部变量count,使用static说明符修饰,表明这个变量只初始化一次,以后每次调用时都会使用上一次的值,造成递增的效果。注意,static修饰的变量初始化时,只能赋值为常量,不能赋值为变量。int i = 3; static int j = i; // 错误上面示例中,j属于静态变量,初始化时不能赋值为另一个变量i。另外,在块作用域中,static声明的变量有默认值0。static int foo; // 等同于 static int foo = 0;static可以用来修饰函数本身。static int Twice(int num) { int result = num * 2; return(result); }上面示例中,static关键字表示该函数只能在当前文件里使用,如果没有这个关键字,其他文件也可以使用这个函数(通过声明函数原型)。static也可以用在参数里面,修饰参数数组。int sum_array(int a[static 3], int n) { // ... }上面示例中,static对程序行为不会有任何影响,只是用来告诉编译器,该数组长度至少为3,某些情况下可以加快程序运行速度。另外,需要注意的是,对于多维数组的参数,static仅可用于第一维的说明。const 说明符函数参数里面的const说明符,表示函数内部不得修改该参数变量。void f(int* p) { // ... }上面示例中,函数f()的参数是一个指针p,函数内部可能会改掉它所指向的值*p,从而影响到函数外部。为了避免这种情况,可以在声明函数时,在指针参数前面加上const说明符,告诉编译器,函数内部不能修改该参数所指向的值。void f(const int* p) { *p = 0; // 该行报错 }上面示例中,声明函数时,const指定不能修改指针p指向的值,所以*p = 0就会报错。但是上面这种写法,只限制修改p所指向的值,而p本身的地址是可以修改的。void f(const int* p) { int x = 13; p = &x; // 允许修改 }上面示例中,p本身是可以修改,const只限定*p不能修改。如果想限制修改p,可以把const放在p前面。void f(int* const p) { int x = 13; p = &x; // 该行报错 }如果想同时限制修改p和*p,需要使用两个const。void f(const int* const p) { // ... }可变参数有些函数的参数数量是不确定的,声明函数的时候,可以使用省略号...表示可变数量的参数。int printf(const char* format, ...);上面示例是printf()函数的原型,除了第一个参数,其他参数的数量是可变的,与格式字符串里面的占位符数量有关。这时,就可以用...表示可变数量的参数。注意,...符号必须放在参数序列的结尾,否则会报错。头文件stdarg.h定义了一些宏,可以操作可变参数。(1)va_list:一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用。(2)va_start:一个函数,用来初始化可变参数对象。它接受两个参数,第一个参数是可变参数对象,第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。(3)va_arg:一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,第一个是可变参数对象,第二个是当前可变参数的类型。(4)va_end:一个函数,用来清理可变参数对象。下面是一个例子。double average(int i, ...) { double total = 0; va_list ap; va_start(ap, i); for (int j = 1; j <= i; ++j) { total += va_arg(ap, double); } va_end(ap); return total / i; }上面示例中,va_list ap定义ap为可变参数对象,va_start(ap, i)将参数i后面的参数统一放入ap,va_arg(ap, double)用来从ap依次取出一个参数,并且指定该参数为 double 类型,va_end(ap)用来清理可变参数对象。
2023年09月20日
5 阅读
0 评论
0 点赞
1
...
15
16
17
...
35