一段C语言代码是怎么执行的
C语言代码的执行可以分为几个主要步骤,包括编译、链接和运行。以下是一段简单的C语言代码的执行过程:
- 编写代码: 首先,程序员编写C语言代码,包括变量声明、函数定义、控制结构等。编写的代码被保存在源文件中,通常使用 .c 扩展名。
- 预处理(Preprocessing): 在编译之前,C编译器会进行预处理。预处理阶段包括处理 #include 指令,宏展开,条件编译等。
- C语言源程序的处理通常包括两个主要步骤:编写代码和预处理。编写代码和预处理通常是一体的,编译器会在编译源代码时自动进行预处理。
- 编译(Compilation): 预处理后的代码经过编译器编译,生成目标文件。目标文件包含了机器代码的二进制表示,但还没有被链接到最终的可执行文件中。(生成.obj的二进制文件)
- 链接(Linking): 在链接阶段,将目标文件与其他必要的目标文件或库文件结合,生成最终的可执行文件。(生成一个.exe的二进制可执行文件)
- 运行(Execution): 最终,用户可以运行生成的可执行文件。
数据类型
说 明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 | 指针 |
数据类型 | char | short | int | long | float | double | |
长 度 | 1 | 2 | 4 | 4 | 4 | 8 | 4 |
转义字符
字符集(Character Set)为每个字符分配了唯一的编号,我们不妨将它称为编码值。在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。这种使用编码值来间接地表示字符的方式称为转义字符
转义字符以\或者\x开头,以\开头表示后跟八进制形式的编码值,以\x开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
\n和\t是最常用的两个转义字符:
- \n用来换行,让文本从下一行的开头输出,
- \t用来占位,一般相当于四个空格,或者 tab 键的功能。
单引号、双引号、反斜杠是特殊的字符,不能直接表示:
- 单引号是字符类型的开头和结尾,要使用\'表示,也即"abc\'123";
- 双引号是字符串的开头和结尾,要使用\"表示,也即"abc\"123";
- 反斜杠是转义字符的开头,要使用\\表示,也即'\\',或者"abc\\123"。
- printf("%%d"); 输出%d
标识符
标识符就是程序员自己起的名字,除了变量名,后面还会讲到函数名、宏名、结构体名等,它们都是标识符。不过,名字也不能随便起,要遵守规范;C语言规定,标识符只能由字母(A~Z, a~z)、数字(0~9)和下划线(_)组成,并且第一个字符必须是字母或下划线,不能是数字。
逗号运算符
x = (y = 52, z = 26, k = 32); 这个表达式的目的是将k的值赋给x。
这个表达式实际上是一个括号中的多个赋值语句。在C++中,逗号运算符会依次计算括号中的每个表达式,并返回最后一个表达式的值。
因此,首先执行的是 y = 52,然后是 z = 26,最后是 k = 32。这些赋值操作都会成功,但只有最后的 k = 32 会影响到x的值。
换句话说,这个表达式的逻辑等同于 x = k;,只是通过一系列的逗号赋值操作来实现。这样的写法可能会让读者感到困惑,除非有特定的目的或上下文,否则通常更推荐直接使用 x = k; 这样的赋值语句。
在 a = 2 * 6, a * 3, a + 5; 这个语句中,逗号操作符确实会返回最后一个表达式的值。但在执行完这条语句后,a 的值仍然是最后一个表达式 a + 5 的结果。 在这个语句中,逗号操作符的作用是执行一系列表达式,但只有最后一个表达式的值被赋给 a。因此,在执行 a = 2 * 6, a * 3, a + 5; 后,a 的值为 a + 5 的结果。 这个语句的执行过程可以拆解为: 1.a = 2 * 6; 设置 a 的值为 12。 2.a * 3; 计算 a * 3 的值,但这个值没有被存储或使用。 3.a + 5; 计算 a + 5 的值,返回最后一个表达式的值 因此,a 的值最终是 a + 5 的结果,即 12 + 5 = 17。
自增自减
字符串
char str[] = "http://c.biancheng.net";
char *str = "http://c.biancheng.net";
都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?([]的优先级大于*)
有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
短路运算以及逻辑运算
&和 | 既是逻辑运算符也是位运算符,而&&和||只是逻辑运算符。
- 当&与&&同为逻辑运算符时,它们都用于连接两个Boolean类型的表达式,当&和&&的两端表达式同时为真时,表达式的结果为真,只要有一端为假,那么表达式结果为假。从用法上来看,&和&&并没有什么区别,比如我们可以写两个表达式:
3>5&3>2; 3>5&&3>2;
两个运算符都可以这么用,但是不同的是,当在判断这个表达式的真或假的时候,两者的判断次数不同:
- 当使用&运算符: 计算机在判断表达式的值的时候,先判断3>5 的值为假,然后再判断3>2的结果为真,于是最后的结果是 假&真 为假;
- 但是当我们使用&&运算符的时候:计算机先判断3>5 的值为假,此时表达式的结果一定为假,所以计算机就不再往下判断了,判定表达式结果为假。
- 总结逻辑运算符&与&&的区别是:
- & 无论左边结果是什么,右边还是继续运算;
- &&当左边为假,右边不再进行运算。
- 但是两者的结果是一样的。
当|和||的两端表达式同时为假时,表达式的结果为假,只要有一端为真,那么表达式结果为真。
所以同理,我们可以知道|与||的区别: | 无论左边结果是什么,右边还是继续运算; ||当左边为真,右边不再进行运算。
但是两者的结果是一样的。 所以&&和||是比较高效那么一点点。
格式控制符
格式控制符 | 说明 |
%c | 输出一个单一的字符 |
%hd、%d、%ld | 以十进制、有符号的形式输出 short、int、long 类型的整数 |
%f、%lf | 以十进制的形式输出 float、double 类型的小数 |
%e、%le%E、%lE | 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。 |
%s | 输出一个字符串 |
%-9d中,d表示以十进制输出,9表示最少占9个字符的宽度,宽度不足以空格补齐,-表示左对齐。综合起来, 所以%-9d表示以十进制输出,左对齐,宽度最小为9个字符。
%d称为格式控制符,它指明了以何种形式输出数据。格式控制符均以%开头,后跟其他字符。%d 表示以十进制形式输出一个整数。除了 %d,printf 支持更多的格式控制,例如:
- %c:输出一个字符。c 是 character 的简写。
- %s:输出一个字符串。s 是 string 的简写。
- %f:输出一个小数。f 是 float 的简写。
short、int、long 是C语言中常见的整数类型,其中 int 称为整型,short 称为短整型,long 称为长整型。
- %f 以十进制形式输出 float 类型;
- %lf 以十进制形式输出 double 类型;
- %f和%lf缺省小数点后有6个零
输入输出(非重点但需了解)
在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:
- puts():只能输出字符串,并且输出结束后会自动换行
- putchar():只能输出单个字符
- printf():可以输出各种类型的数据
printf() 格式控制符的完整形式如下:
%[flag][width][.precision]type [ ] 表示此处的内容可有可无,是可以省略的。
1) type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;再如,%-9d中 type 对应 d。
type 这一项必须有,这意味着输出时必须要知道是什么类型。
2) width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,%-9d中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。
当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
3) .precision 表示输出精度,也就是小数的位数。
- 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
- 当小数部分的位数小于 precision 时,会在后面补 0。
4) flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。下表列出了 printf() 可以用的 flag:
标志字符 | 含 义 |
- | -表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。 |
+ | 用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。 |
在C语言中,有多个函数可以从键盘获得用户输入:
- scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
- getchar()、getche()、getch():这三个函数都用于输入单个字符。
- gets():获取一行数据,并作为字符串处理。
对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
其实 scanf 和 printf 非常相似,只是功能相反罢了:
scanf("%d %d", &a, &b); // 获取用户输入的两个整数,分别赋值给变量 a 和 b printf("%d %d", a, b); // 将变量 a 和 b 的值在显示器上输出
它们都有格式控制字符串,都有变量列表。不同的是,scanf 的变量前要带一个&符号。&称为取地址符,也就是获取变量在内存中的地址。
scanf 会根据地址把读取到的数据写入内存。
直接把 char *c; 没有分配地址就使用scnaf("%s",c)是错误的 scanf 函数实际上是给指针赋值的,但关键在于这个指针必须指向一个已经分配了内存的区域。在 C 语言中,数组和指针是相关的。当我们声明一个数组时,例如 char c[100];,我们实际上是声明了一个可以存储 100 个字符的内存区域,并返回一个指向这个区域的指针。所以,当我们使用 scanf 函数时,我们实际上是给这个指针所指向的内存区域赋值。 如果你只声明一个 char *c;,那么 c 是一个指向字符的指针,但它并没有指向任何已经分配了内存的区域。所以,如果你试图使用 scanf 函数给这样的指针赋值,那么结果将是未定义的,可能会导致程序崩溃。 所以,如果你想要使用 scanf 函数给一个字符指针赋值,你需要先为这个指针分配足够的内存,例如通过声明一个字符数组。
scanf() 检查缓冲区的规则
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:
- 如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符\n
- 如果有数据,那就看是否符合控制字符串的规则:
- 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了。
- 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据。
- 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
- 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程。
- 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。
getchar()最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)的替代品,除了更加简洁,没有其它优势了;
或者说,getchar() 就是 scanf() 的一个简化版本。
gets() 和 scanf() 的主要区别是:
- scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
- gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。
也就是说,gets() 能读取含有空格的字符串,而 scanf() 不能。
二维数组
在C语言中,二维数组是按行排列的。也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4)=48 个字节。
对于二维数组的初始化还要注意以下几点:
1) 可以只对部分元素赋值,未赋值的元素自动取“零”值。
2) 如果对全部元素赋值,那么第一维的长度可以不给出。例如:int a[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
字符数组实际上是一系列字符的集合,也就是字符串(String)。在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。
字符串初始化
C语言规定,可以将字符串直接赋值给字符数组,例如:
char str[30] = {"www.cdsy.xyz"};
char str[30] = "www.cdsy.xyz"; //这种形式更加简洁,实际开发中常用
这里需要留意一个坑,字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:
char str[7]; str = "abc123"; //错误
str[0] = 'a'; str[1] = 'b'; str[2] = 'c'; str[3] = '1'; str[4] = '2'; str[5] = '3'; //正确
原因在于字符数组在定义后是不可被整体赋值的。这是因为C++中对数组的赋值操作是不允许的,你需要逐个元素地赋值或使用字符串拷贝函数。 类似于string类中的初始化函数,只对象初始化时起作用
在C语言中,字符串总是以'\0'作为结尾,所以'\0'也被称为字符串结束标志,或者字符串结束符。
'\0'是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。
C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到'\0'就认为到达了字符串的末尾,就结束处理。'\0'至关重要,没有'\0'就意味着永远也到达不了字符串的结尾。
由" "包围的字符串会自动在末尾添加'\0'。例如,"abc123"从表面看起来只包含了 6 个字符(也就是6个字节),其实不然,C语言会在最后隐式地添加一个'\0',(变成7个字节)这个过程是在后台默默地进行的,所以我们感受不到。 需要注意的是,逐个字符地给数组赋值并不会自动添加'\0',一定要加str[6] = '\0'; // 添加字符串结束符否则出现乱码,且字符数组的长度至少要比字符串的长度大 1,用于存储'\0'
str[i] = 0; //此处为添加的代码,也可以写作 str[i] = '\0'; 根据 ASCII 码表,字符'\0'的编码值就是 0。
字符串长度,就是字符串包含了多少个字符(不包括最后的结束符'\0')。例如"abc"的长度是 3,而不是 4。
在C语言中,我们使用string.h头文件中的 strlen() 函数来求字符串的长度
strlen 和 sizeof 区别
strlen 是 C/C++ 标准库中的一个函数,用于计算字符串的长度。它接受一个字符串(实际上是一个字符指针)作为参数,并返回该字符串中字符的数量,不包括结束字符 '\0'。在你的例子中,strlen("abcde") 将返回 5。
sizeof 是 C/C++ 中一个运算符,用于获取数据类型或变量所占用的内存空间大小。它返回的是字节数。对于一个字符串来说,sizeof 返回的是整个字符串(包括结束字符)占用的字节数。在你的例子中,如果 s 是一个字符数组(也就是字符串),那么 sizeof(s) 将返回 数组开辟的空间大小。注意,如果你将字符串作为指针处理,例如 char *s = "abcde"; sizeof(s),这会返回指针的大小,而不是字符串的长度。返回 4 字节;
sizeof(int) 是一个表达式计算,而不是函数调用。sizeof 是一个运算符,它用于计算数据类型(如 int)所占用的内存空间大小。
所以,如果你想获取字符串的长度(不包括 '\0'),你应该使用 strlen;如果你想获取字符串占用的字节数(包括 '\0'),你应该使用 sizeof。
字符串相关函数
字符串连接函数 strcat()把两个字符串拼接在一起 | strcat(arrayName1, arrayName2); | arrayName1、arrayName2 为需要拼接的字符串。 | strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志'\0'。这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界(超出范围)。 | strcat() 的返回值为 arrayName1 的地址。 |
字符串复制函数 strcpy() | strcpy(arrayName1, arrayName2); | strcpy()是深拷贝 | strcpy() 会把 arrayName2 中的字符串拷贝到 arrayName1 中,字符串结束标志'\0'也一同拷贝。 | |
字符串比较函数 strcmp() | strcmp(arrayName1, arrayName2); | 若 arrayName1 和 arrayName2 相同,则返回0; | 字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。 | 若 arrayName1 大于 arrayName2,则返回大于 0 的值;若 arrayName1 小于 arrayName2,则返回小于0 的值。(随机的值) |
数组赋值可以涉及到两种主要的拷贝方式:浅拷贝和深拷贝
1. 浅拷贝: 当你将一个数组的值赋给另一个数组时,如果这两个数组是通过直接赋值或指针赋值而产生的,那么这是浅拷贝。在浅拷贝中,只是复制了数组的元素,但如果数组包含指针等引用类型,它们的指向仍然相同。因此,对一个数组的修改可能会影响到另一个数组。
int sourceArray[] = {1, 2, 3, 4, 5};
int destinationArray[5];
for (int i = 0; i < 5; ++i) {
destinationArray[i] = sourceArray[i];
}
2. 深拷贝:如果你通过其他手段(如使用 `std::copy` 函数或复制每个元素的值)来将一个数组的值赋给另一个数组,这可能导致深拷贝。在深拷贝中,每个数组都有自己独立的内存副本,对一个数组的修改不会影响到另一个数组。
#include
int sourceArray[] = {1, 2, 3, 4, 5};
int destinationArray[5];
std::copy(std::begin(sourceArray), std::end(sourceArray), std::begin(destinationArray));
通常来说,在 C++ 中,对数组的赋值操作更接近浅拷贝的概念,因为数组名在很多情况下会退化为指向数组首元素的指针,直接赋值会共享相同的内存地址。深拷贝需要更为明确的拷贝操作,通常涉及到使用函数、算法或者自定义的拷贝操作。
C语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能会发生问题。
越界访问的数组元素的值都是不确定的,没有实际的含义,因为数组之外的内存我们并不知道是什么,可能是其它变量的值,可能是函数参数,可能是一个地址,这些都是不可控的。
函数的定义
dataType functionName(){
//body
}
- dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
- functionName 是函数名,它是标识符的一种,命名规则和标识符相同。函数名后面的括号( )不能少。
- 返回值类型和函数名统称为函数头部
- body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由{ }包围。
- 如果有返回值,在函数体中使用 return 语句返回。return 出来的数据的类型要和 dataType 一样。
形参与实参
函数定义时给出的参数称为形式参数,简称形参;函数调用时传递的数据称为实际参数,简称实参。
函数调用时,将实参的值传递给形参,相当于一次赋值操作。
1) 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。 2) 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。 3) 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。 4) 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。
强调一点,C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中允许出现对另外一个函数的调用。
一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。main() 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到 main() 函数,最后由 main() 函数结束整个程序。
函数声明
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
static变量详解
在 C 语言中,static 关键字不仅可以用来修饰变量,还可以用来修饰函数。在使用 static 关键字修饰变量时,我们称此变量为静态变量。
静态变量的存储方式与全局变量一样,都是静态存储方式。但这里需要特别说明的是,静态变量属于静态存储方式,属于静态存储方式的变量却不一定就是静态变量。例如,全局变量虽然属于静态存储方式,但并不是静态变量,它必须由 static 加以定义后才能成为静态全局变量。
在C语言中,变量的存储类别(Storage Class)决定了变量的生命周期和作用域,其中静态存储类别是其中之一。静态存储类别的变量在程序的整个生命周期内都存在,而不仅仅是在函数调用期间。
在C语言中,有两种静态存储类别的变量:
- 全局变量(Global Variables):
- 定义在函数外部的变量是全局变量。
- 它们默认具有静态存储类别,即在整个程序的执行期间都存在。
- 全局变量如果没有被加上 static关键字,可以在其他文件中通过外部链接访问。
#include <stdio.h>
int num;
2.静态局部变量(Static Local Variables):
- 定义在函数内部,但使用 static关键字修饰的变量是静态局部变量。
- 静态局部变量也具有静态存储类别,它们在整个程序执行期间都存在,但作用域仅限于包含它们的函数。
- 静态局部变量在函数调用之间保持其值。
void exampleFunction() {
static int staticLocalVariable; // 静态局部变量
}
所以,虽然全局变量和静态局部变量都属于静态存储方式,但它们的作用域和生命周期有所不同。全局变量的作用域是整个程序,而静态局部变量的作用域仅限于包含它们的函数。
隐藏与隔离的作用
如果我们希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用,也就是说限制其作用域只在定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用。这时,就可以通过在全局变量之前加上关键字 static 来实现,使全局变量被定义成为一个静态全局变量。这样就可以避免在其他源文件中引起的错误。也就起到了对其他源文件进行隐藏与隔离错误的作用,有利于模块化程序设计。
保持变量内容的持久性
有时候,我们希望函数中局部变量的值在函数调用结束之后不会消失,而仍然保留其原值。即它所占用的存储单元不释放,在下一次调用该函数时,其局部变量的值仍然存在,也就是上一次函数调用结束时的值。这时候,我们就应该将该局部变量用关键字 static 声明为“静态局部变量”。
当将局部变量声明为静态局部变量的时候,也就改变了局部变量的存储位置,即从原来的栈中存放改为静态存储区存放。这让它看起来很像全局变量,其实静态局部变量与全局变量的主要区别就在于可见性,静态局部变量只在其被声明的代码块中是可见的。
对某些必须在调用之间保持局部变量的值的子程序而言,静态局部变量是特别重要的。如果没有静态局部变量,则必须在这类函数中使用全局变量,由此也就打开了引入副作用的大门。使用静态局部变量最常见的题目就是实现统计次数的功能,如下面示例所示。
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件(即一个项目),包括 .c 和 .h 文件。如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了。
局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。
1) 在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。
2) 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。
3) 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
4) 在语句块中也可定义变量,它的作用域只限于当前语句块。(复合语句for/if中由{ }包围起来的代码(代码块)以及 for 循环条件里面定义新变量,这样的变量也是块级变量,它的作用域仅限于 for 循环内部。)
预处理命令
以#号开头的命令称为预处理命令。
#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,(在宏内不会进行计算之类的操作只会简单替换)这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
2) 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
3) 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题,因为宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存
指针
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身,这个时候*称为指针运算符,即用来取得某个地址上的数据。(*表示指向某某,&表示取某某地址)
在C语言中,我们将第 0 个元素的地址称为数组的首地址。
*(arr+i)这个表达式,arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[i]。注意没有arr++或++arr这种操作
arr 是int*类型的指针,每次加 1 时它自身的值会增加 sizeof(int),加 i 时自身的值会增加 sizeof(int) * i
1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
2) 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。
在C语言中,int* a[10]和int (*a) [10]这两种声明方式在语义上是不同的。
int* a[10]:这是一个名为a的数组,数组的每个元素都是一个指向整数的指针。也就是说,这是一个指针数组,数组的每个元素都是一个可以存储整数地址的变量。
int (*a) [10]:这是一个名为a的指针,该指针指向一个包含10个整数的数组。等价于 int b[10]; int *a = b; 也就是说,这是一个指向数组的指针,它存储的是数组的首个元素的地址。
int (*p)[4] = a;
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针
对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。
二维数组指针
首先我们必须明白二维数组只是在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
从概念上理解,a 的分布像一个矩阵:
但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:
由图可知在C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。
并且C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。
在涉及C语言的题目中我们经常会遇到指向 数组 的指针变量 即int (*p)[4] = a;(括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。)
对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。
实例:
1) p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行(下标从零开始)。
2)*(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素
3) *(p+1)+1表示第 1 行第 1 个元素的地址, *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
4) *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。
根据上面的结论,可以很容易推出以下的等价关系:
a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
常见指针变量的定义
定 义 | 含 义 |
int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。 |
int **p; | p 为二级指针,指向 int * 类型的数据。 |
int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
int (*p)[n]; | p 为二维指针 |
int *p(); | p 是一个函数,它的返回值类型为 int *。 |
int (*p)(); | p 是一个函数指针,指向原型为 int func() 的函数。 |
- 指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。
- 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃,一定要加引用
- 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL,链表中经常使用
- 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
- 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。
运算符优先级和结合性
优先级常考点
优先级问题 | 表达式 | 经常误认为的结果 | 实际结果 |
. 的优先级高于 *(-> 操作符用于消除这个问题) | *p.f | p 所指对象的字段 f,等价于:(*p).f | 对 p 取 f 偏移,作为指针,然后进行解除引用操作,等价于:*(p.f) |
[] 高于 * | int *ap[] | ap 是个指向 int 数组的指针,等价于:int (*ap)[] | ap 是个元素为 int 指针的数组,等价于:int *(ap []) |
函数 () 高于 * | int *fp() | fp 是个函数指针,所指函数返回 int,等价于:int (*fp)() | fp 是个函数,返回 int*,等价于:int* ( fp() ) |
== 和 != 高于位操作 | (val & mask != 0) | (val &mask) != 0 | val & (mask != 0) |
== 和 != 高于赋值符 | c = getchar() != EOF | (c = getchar()) != EOF | c = (getchar() != EOF) |
算术运算符高于位移 运算符 | msb << 4 + lsb | (msb << 4) + lsb | msb << (4 + lsb) |
逗号运算符在所有运 算符中优先级最低 | i = 1, 2 | i = (1,2) | (i = 1), 2 |
结构体
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
};
像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
很多人区分不了结构体名和结构体变量,这是考试一个热点
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
标签:函数,int,命题,数组,字符串,指针,热点,变量,考研 From: https://blog.51cto.com/u_16491666/9536281