首页
前端面试题
前端报错总结
电子书
更多
插件下载
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语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
41
篇与
的结果
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 点赞
2023-09-20
指针
指针指针是 C 语言最重要的概念之一,也是最难理解的概念之一。简介指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。字符*表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char*表示一个指向字符的指针,float*表示一个指向float类型的值的指针。int* intPtr;上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。int *intPtr; int * intPtr; int* intPtr;本书使用星号紧跟在类型关键字后面的写法(即int* intPtr;),因为这样可以体现,指针变量就是一个普通变量,只不过它的值是内存地址而已。这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。// 正确 int * foo, * bar; // 错误 int* foo, bar;上面示例中,第二行的执行结果是,foo是整数指针变量,而bar是整数变量,即*只对第一个变量生效。一个指针指向的可能还是指针,这时就要用两个星号**表示。int** foo;上面示例表示变量foo是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。* 运算符*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。void increment(int* p) { *p = *p + 1; }上面示例中,函数increment()的参数是一个整数指针p。函数体里面,*p就表示指针p所指向的那个值。对*p赋值,就表示改变指针所指向的那个地址里面的值。上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。& 运算符&运算符用来取出一个变量所在的内存地址。int x = 1; printf("x's address is %p\n", &x);上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址。上一小节中,参数变量加1的函数,可以像下面这样使用。void increment(int* p) { *p = *p + 1; } int x = 1; increment(&x); printf("%d\n", x); // 2上面示例中,调用increment()函数以后,变量x的值就增加了1,原因就在于传入函数的是变量x的地址&x。&运算符与*运算符互为逆运算,下面的表达式总是成立。int i = 5; if (i == *(&i)) // 正确指针变量的初始化声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存空间里面的值是随机的,也就是说,指针变量指向的值是随机的。这时一定不能去读写指针变量指向的地址,因为那个地址是随机地址,很可能会导致严重后果。int* p; *p = 1; // 错误上面的代码是错的,因为p指向的那个地址是随机的,向这个随机地址里面写入1,会导致意想不到的结果。正确做法是指针变量声明后,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的初始化。int* p; int i; p = &i; *p = 13;上面示例中,p是指针变量,声明这个变量后,p会指向一个随机的内存地址。这时要将它指向一个已经分配好的内存地址,上例就是再声明一个整数变量i,编译器会为i分配内存地址,然后让p指向i的内存地址(p = &i;)。完成初始化之后,就可以对p指向的内存地址进行赋值了(*p = 13;)。为了防止读写未初始化的指针变量,可以养成习惯,将未初始化的指针变量设为NULL。int* p = NULL;NULL在 C 语言中是一个常量,表示地址为0的内存空间,这个地址是无法使用的,读写该地址会报错。指针的运算指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。(1)指针与整数值的加减运算指针与整数值的运算,表示指针的移动。short* j; j = (short*)0x1234; j = j + 1; // 0x1236上面示例中,j是一个指针,指向内存地址0x1234。由于0x1234本身是整数类型(int),跟j的类型(short*)并不兼容,所以强制使用类型投射,将0x1234转成short*。你可能以为j + 1等于0x1235,但正确答案是0x1236。原因是j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。同样的,j - 1得到的结果是0x1232。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。(2)指针与指针的加法运算指针只能与整数值进行加减运算,两个指针进行加法是非法的。unsigned short* j; unsigned short* k; x = j + k; // 非法上面示例是两个指针相加,这是非法的。(3)指针与指针的减法相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。这时,减法返回的值属于ptrdiff_t类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。short* j1; short* j2; j1 = (short*)0x1234; j2 = (short*)0x1236; ptrdiff_t dist = j2 - j1; printf("%td\n", dist); // 1上面示例中,j1和j2是两个指向 short 类型的指针,变量dist是它们之间的距离,类型为ptrdiff_t,值为1,因为相差2个字节正好存放一个 short 类型的值。(4)指针与指针的比较运算指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1(true)或0(false)。
2023年09月20日
5 阅读
0 评论
0 点赞
1
...
11
12
13
14