C语言
一、基础知识
(1)C语言关键字
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static switch typedef union unsigned void volatile while
- auto——自动的,所有的局部变量都是auto修饰的,这些变量自动创建自动销毁。
- extern——外部的,用于声明外部变量
- register——寄存器关键字
- typedef——类型重命名
- inline——内联函数说明符
- _Noreturn——函数说明符,表明函数完成调用后不再返回主调函数
- _Static_assert——断言关键字,断言出现问题则不通过编译,打印错误信息
include 和define 都不是关键字,它们是预处理指令。
(2)C程序的主要部分
? C预处理器为预处理器指令(以#符号开始)查找源代码程序,并在开始编译程序之前处理它们。
# define SOME 1
# include <stdio.h>
void function(void);
int main()
{
int n;
printf("Hello World!");
function();
return 0;
}
void function(void)
{
printf("Other function");
return 0;
}
(3)转义序列 escape sequence
(4)基本运算符
? 基本运算符常用的包括,单目运算符:
双目操作符中的关系运算符:
1.赋值运算符=
C语言允许多重赋值
int a, b, c;
a = b = c = 98;
赋值表达式的值是赋值运算符左侧运算对象的值。例如:
while((ch = getchar()) != '\n')
ch = getchar() 这句赋值表达式的值是ch的值,然后再判断ch != '\n' ,用于流程控制。
2.算术运算符+ 、- 、* 、/
C语言中整数除法结果的小数部分会被直接丢弃,而不是四舍五入,这一过程称为截断。
printf("integer division 5/4 is %d", 5/4);
3.逗号运算符,
逗号运算符有两个性质,首先,它保证了被它分隔的表达式是从左往右求值(换言之,逗号是一个序列点,所以逗号左侧项的所有副作用都在程序执行逗号右侧项之前发生)。其次,整个逗号表达式的值是右侧项的值。
x = (y = 3, (z=++y+2) + 5)
逗号还可以用作分隔符,这是它最常用的用法。
char ch, date;
4.逻辑运算符&& 、|| 、!
C99标准新增了可代替逻辑运算符的拼写,它们被定义在iso646.h头文件中,如果在程序中包含该头文件,便可以用and 替代&& ,用or 替代|| ,用not 替代! ,这就和python的逻辑运算符一致。
# include <iso646.h>
and == &&;
or == ||;
not == !;
逻辑表达式的求值顺序是从左往右的。一旦发现有使整个表达式为假的因素,立即停止剩余部分的求值。
5.条件运算符? (三目运算符):
条件运算符(三目运算符)是C语言中唯一的三元运算符。条件表达式的通用形式如下:
expression1 ? expression2 : expression3
如果expression1为真,那么整个条件表达式的值与expression2的值相同;如果expression1为假,那么整个条件表达式的值和expression3相同。
(5 > 3) ? 1 : 0;
与python的三元表达式类似。
6.位运算符<< 、>> 、^ 、~ 、& 、| :
左移<< ,右移>> ,按位异或^ ,按(二进制)位取反~ ,& 按位与,| 按位或。
左移的具体操作是左边丢弃,右边补零,右移的具体操作是右边丢弃,对于无符号数左边补零,对于有符号数左边补充符号位。
int a, b =2;
a = b << 1;
a = b >> 1;
a = ~b;
7.其他运算符
7.1sizeof 运算符和size_t类型
sizeof运算符以字节为单位返回运算对象的大小,C语言规定,sizeof返回size_t类型的值,这是一个无符号类型,但它并不是新的类型。sizeof后面的括号能够省略,这就说明sizeof是一个操作符(运算符)而不是函数,但是当sizeof需要取类型的字节大小的时候,括号不能省略。sizeof运算符可以计算变量或者类型所占的字节大小,但是需要注意sizeof所包含的表达式是不参与计算的。
int n = 0;
size_t intsize;
intsize = sizeof(int);
7.2 求模运算符%
求模运算符只能用于整数而不能用于浮点数。
7.3 递增运算符++
++ 拥有前缀模式和后缀模式,前缀模式和后缀模式的区别为:
递增运算符可以方便流程控制
int num = 0;
while (num++ < 10)
{
printf("curretn num is %d", num);
}
7.4 递减运算符--
-- 的使用和++ 一致,递增运算和递减运算都有很高的优先级,只有() 的优先级高于它们。
7.5 其他赋值运算符+=、-=、*=、/=、%= 和&=、|=、^=、>>=、<<=
7.6 下标引用[] ,函数调用() ,结构成员. 和-> 运算符:
struct Stu
{
char name[30];
int age;
double float;
};
struct Stu s = {"张三", 22, 99.9};
s.name, s.age, s.score
(5)复合语句 compound statement
? 复合语句是用花括号括起来的一条或者多条语句,复合语句也称为块(Block)。C语言与Python不同,在C语言中缩进对编译器不起作用,编译器通过花括号{} 来识别块,解析指令。使用缩进的目的是为了养成良好的代码风格,方便阅读。
(6)空语句
? 单独的一个; 会被视作为一条什么都不处理的空语句 null statement。
二、数据类型
(1)整数类型
基本的整数类型包括:
-
int -
char 字符实际上是作为整数进行存储的。ASCII码。 -
_Bool _Bool 类型的变量只能存储1(真)0(假)。如果把其他非零数值赋给_Bool 类型变量,该变量会被设置为1。C99提供了stdbool.h头文件,该头文件让bool称为_Bool的别名,而且还把true和false分别定义为1和0的符号常量。包含该头文件后,写出的代码可以与C++兼容,因为C++把bool、true和false定义为关键字。 # include <stdio.h>
# include <stdbool.h>
int main(void)
{
printf("true is %d, false is %d", true, false)
return 0;
}
**a. **在int的基础上有:
- int 有符号整型
- short int (short) 短整型
- unsigned short int (unsigned short) 无符号短整型
- long int (long) 长整型
- unsigned long int (unsigned long)
- long long int (long long) 更长整型
b. 在char的基础上有:
(2)浮点数类型
(3)类型所占字节大小
(4)类型转换
C语言具有自动类型转换的机制,但是自动类型转换往往容易出错,使用强制类型转换可以避免这种错误。强制类型转换符是在某个量前放置用圆括号() 括起来的类型名,该类型名即是希望转化成的目标类型。圆括号() 和它括起来的类型构成了强制类型转换运算符。通用形式是(type)
mice = 1.6 + 1.7;
mice = (int)1.6 + (int)1.7;
除了常规的类型转换之外,C语言还具有隐式类型转换,俗称(整型提升)。
**如何进行整型提升的呢?有符号的整型进行提升是按照数据的符号位来进行提升的,如果为正那么通过补0 来进行提升,如果为负那么通过补1 来进行提升。无符号的整型进行提升全部通过补0 来进行提升。**举个列子:
int main(void)
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n",c);
return 0;
}
这是因为char类型占一个字节共8位,取值在-127~127之间,127+3的二进制结果为1000010,转换为整型那么就是-126。
三、字符串和格式化输入输出
(1)单字符字面量与字符串字面量
? C语言中单字符字面量存储在char类型中,但是C语言没有专门用于存储字符串的数据格式,通常使用char的数组来存储字符串,单字符字面量用'' 单引号来表示,字符串字面量用"" 双引号来表示:
char bit = 'a';
char bit8[8] = "abcdefgh";
? 双引号中的字符和编译器自动加入末尾的\0 字符,都作为字符串存储在内存中,如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量。例如:
char greeting[50] = "Hello, and" " how are you" " today!";
char greeting[50] = "Hello, and how are you totday!";
字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被存储一次,即使函数被调用多次,并在整个程序的生命周期内存在。用双引号括起来的内容被视为指向该字符串存储位置的指针。例如:
"space of string";
printf("%c", *"space of string");
还可以使用指针表示法、数组表示法创建字符串,例如:
const char *ptr1 = "Something is pointing at me.";
const char ar1[] = "Something is pointing at me.";
总之,初始化数组是把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。数组表示法创建的数组名是常量,而指针表示法创建的指针是变量。 两者都可以通过数组来表示,可以对数组进行修改等(因为,数组的元素是变量,但是数组名不是变量),并且可以进行指针的加减法,但是只有使用指针表示法创建的指针变量才能进行递增和递减的操作。数组是对内存中变量的一种拷贝(副本),而指针是对内存中变量的一种引用。
(2)格式化输入输出函数scanf\fscanf\sscanf\printf\fprintf\sprintf
功能 | 函数 | 头文件 | 函数细节 |
---|
打印字符输出 | putchar | stdio.h | 输出单个字符,字符使用'' | 获取字符输入 | getchar | stdio.h | 获得单个字符输入,字符使用'' | 打印字符串输出 | puts | stdio.h | 输出单行的字符,直到遇到\0 并且在末尾自动添加换行符\n 。 | 获取字符串输入 | gets | stdio.h | 获取整行的输入,它读取整行的输入,直到遇到换行符,然后丢弃换行符, 存储其他字符,并在这些字符的末尾添加一个空字符使其成为一个字符串。 | 格式化输入 | scanf | stdio.h | 针对标准输入的格式化输入函数–stdin | 格式化输入 | fscanf | stdio.h | 针对所有流的格式化输入语句–stdin/文件 | 格式化输入 | sscanf | stdio.h | 从一个字符串中读取格式化输入数据 | 格式化输出 | printf | stdio.h | 针对标准输出的格式化输出函数–stdout | 格式化输出 | fprintf | stdio.h | 针对所有流的格式化输出语句–stdout/文件 | 格式化输出 | sprintf | stdio.h | 把一个格式化的数据转化成字符串 |
1、使用printf()
printf和scanf来自于头文件<stdio.h>
printf和scanf的参数包括,格式化字符串和参数,printf函数具有返回值,返回值是打印的字符长度。
转换说明的修饰符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b43SathP-1655802386019)(D:\Study_Data\Markdown_Note\figure\image-20220424151445735.png)]
printf的标记,与python的格式控制非常类似
2、使用scanf()
scanf()读取基本变量类型的值,在变量名之前需要添加一个&,& 称为地址运算符。scanf()将字符串读入字符数组中,不需要使用&。scanf()函数使用空白(换行符、制表符和空格)把输入分成多个字段。在依次把转化说明和字段匹配时跳过空白。唯一例外的是%c转化说明,根据%c,scanf()会读取每个字符,包括空白。scanf()使用的转换说明和printf()基本一致。scanf 有两种方法确定输入的结束,第一种,如果使用%s 转换说明,以下一个空白字符(空行、空格、制表符和换行符)作为字符的结束。**第二种,如果指定了字段宽度,如%10s 那么scanf() 将读取10个字符或读取到第一个空白字符停止(先满足的条件即是输入结束的条件)。**如果使用带多个转换说明的scanf(),C规定在第一个输错的位置处停止读取。scanf函数具有返回值,scanf的返回值是成功读取项的数量。
3、* 的用法
printf()中的* 可以用来代替,格式控制中的字符指定,例如:
printf("%*.*f", width, precision, 3.14159);
scanf()中的* 放置在%和转换字符之间,可以使scanf()跳过相应的输出项,例如:
int n;
printf("Please enter three integers:\n");
scanf("%*d %*d %d\n", &n);
printf("The last integer is %d\n", n);
4、使用getchar()
getchar()和putchar()函数均来自头文件<stdio.h>,getchar()函数不带任何参数,它从输入队列中返回下一个字符。例如,下面的语句读取下一个字符的输入,并把该字符的值赋值给变量ch:
ch = getchar();
scanf("%c", &ch);
5、使用putchar()
putchar()函数打印它的参数,例如下面的语句把之前赋值给ch的值作为字符打印出来:
putchar(ch);
printf("%c", ch);
注意getchar()和putchar()是不需要转换说明的,因为它们只能够处理单个字符。
(3)字符串输出函数
功能 | 函数名 | 头文件 | 用途 |
---|
字符输入函数 | fgetc | stdio.h | 所有输入流 | 文本行输入函数 | fgets | stdio.h | 所有输入流 | 格式化输入函数 | fscanf | stdio.h | 所有输入流 | 二进制输入函数 | fwrite | stdio.h | 文件 | 字符输出函数 | fputc | stdio.h | 所有输出流 | 文本行输出函数 | fputs | stdio.h | 所有输出流 | 格式化输出函数 | fprintf | stdio.h | 所有输出流 | 二进制输出函数 | fread | stdio.h | 文件 |
? puts() 输出单行的字符,直到遇到\0 并且在末尾自动添加换行符\n 。
? fputs() 该函数用于处理文件输出,类似于fprintf
(4)字符串输入函数
? gets() 获取整行的输入,它读取整行的输入,直到遇到换行符,然后丢弃换行符,存储其他字符,并在这些字符的末尾添加一个空字符使其成为一个字符串。
? fgets() 该函数专门设计用于处理文件输入,类似于fscanf
(5)字符串函数strcpy\strcmp\strlen\strcat\strchr
字符串函数都包含在头文件string.h 中
功能 | 函数名 | 函数细节 |
---|
计算字符串长度 | strlen | 计算字符串长度遇到\0 停止长度计算 | 字符串拼接 | strcat | 源字符串必须以\0 结尾,并且字符空间必须足够大 | 字符串拼接 | strncat | 可以指定拼接的字符数量 | 字符串比较 | strcmp | 将两个字符串逐个字符进行比较,按照ASCII表进行大小比较,相同返回0, 前者大于后者返回1,后者大于前者返回-1。strcmp比较的是字符串而不是 应该用"" 而不是'' | 字符串比较 | strncmp | 可以指定比较字符串的长度,从字符串的首元素位置开始计算长度 | 字符串拷贝 | strcpy | 将源字符串的数据拷贝到目标字符串中,并且返回目标字符串的指针,且源 字符串指针不必指向数组的开始,这个属性可以拷贝数组的一部分,strcpy 会拷贝\0 | 字符串拷贝 | strncpy | 可以指定拷贝字符串的长度,从字符串的首元素位置开始计算长度 | 字符查找函数 | strchr | 在源字符串中查找目标字符,并且返回目标字符首次被查找的地址 | 字符串查找函数 | strstr | 在源字符串中查找目标字符串,并且返回目标字符串首次被查找的地址 |
更多字符测试函数isupper\islower\toupper\tolower 详见头文件ctype.h
(6)字符转换函数atoi\atof\atol
atoi\atof\atol 分别将字符串转换成整型,浮点型,长整型数值。包含在头文件stdlib.h 中。
功能 | 函数 | 头文件 | 函数细节 |
---|
将字符串转化为整型数值 | atoi | stdlib.h | 返回转换后的数值,如没进行转换则返回0,字符串要以数字打头 | 将字符串转化成浮点型 | atof | stdlib.h | 返回转换后的数值,如没进行转换则返回0,字符串要以数字打头 | 将字符串转化成长整型 | atol | stdlib.h | 返回转换后的数值,如没进行转换则返回0,字符串要以数字打头 | 将字符串按照base进制转换成长整型 | strtol | string.h | 返回转换后的数值,并且保存未进行转换的字符地址到end指针中 | 将字符串按照base进制转换成无符号长整型 | strtoul | string.h | 返回转换后的数值,并且保存未进行转换的字符地址到end指针中 | 将字符串按照base进制转换成双精度浮点型 | strtod | string.h | 返回转换后的数值,并且保存未进行转换的字符地址到end指针中 |
(7)内存操作函数
功能 | 函数 | 头文件 | 函数细节 |
---|
将一块内存的内容拷贝到另一块内存中 | memcpy | string.h | memcpy假定了两块内存之间没有重叠的部分,需要指定内存的字节数,返回目标地址 | 将一块内存的内容拷贝到另一块内存中 | memmove | string.h | memmove不做不重叠的假设,需要指定拷贝内存的字节数返回目标地址 | 按照ASCII表对两块内存中的元素逐个进行比较 | memcmp | string.h | 需要指定比较字节的个数,返回值返回-1、0、1其中的一个 | 将一块内存设置为指定内容 | memset | string.h | 需要指定设定的字节数,返回目标内存的拷贝地址 |
四、流程控制(循环)
(1)while循环
? while循环包含三个部分,首先是while 关键字,然后是() 内的测试条件,最后是{} 内的循环体。如果循环体中只有一条语句则可以不使用{} 括起来。
while (expression)
statement
statement可以是一条简单语句或复合语句。典型的while循环伪代码如下
获得初值
while (值满足测试条件)
{
处理该值
获取下一个值
}
(2) for循环
? for循环包含三个部分,首先是for 关键字,然后是() 内的控制表达式,最后是{} 内的循环体。如果循环体中只有一条语句则可以省略{} 。控制表达式由三部分组成,分别用两个; 隔开,第一个表达式是初始化,只会在for循环开始时执行一次。第二个表达式是测试条件,在执行循环之前对表达式求值,如果表达式为假,循环结束。第三个表达式执行参数更新,在每次循环结束时求值。
for (initialize; test; update)
stetement
statement可以是一条简单语句或复合语句。典型的for循环伪代码如下
for (获得初值; 值满足测试条件; 获取下一个值)
{
处理该值
}
# include <stdio.h>
int main(void)
{
int x;
int y = 55;
for (x = 1; y <= 75; y = (++x * 5) + 50)
printf("%10d %10d\n", x, y);
return 0;
}
1 55
2 60
3 65
4 70
5 75
用这个例子来说明,第三个表达式执行参数更新是在每次循环结束的时候进行。
? for loop的控制表达式中可以省略一个或者多个表达式(但是不能省略; ),只要在循环中包含能结束的语句就可以。
# include <stdio.h>
int main(void)
{
int ans, n;
ans = 2;
for (n = 3; ans <= 25; )
{
ans = ans * n;
printf("n = %d; ans = %d.\n", n, ans);
}
printf("n = %d; ans = %d.\n", n, ans);
return 0;
}
n = 3; ans = 6.
n = 3; ans = 18.
n = 3; ans = 54.
n = 3; ans = 54.
? 但是需要注意一点的是,省略第二个表达式被视为真,循环会一直进行。
# include <stdio.h>
int main(void)
{
for (; ;)
printf("This is a bad loop\n");
return 0;
}
会一直打印This is a bad loop 。
? 第一个表达式不一定是给变量赋初值,也可以使用printf() ,只是要记住,第一个表达式只在循环开始时执行一次。最后一个表达式也不一定是要给变量进行参数更新,也可以使用sacnf() ,只是要记住,最后一个表达式是在每次循环后执行。
# include <stdio.h>
int main(void)
{
int num = 0;
for (printf("Keep entering numbers!\n"); num != 6; scanf("%d", &num));
printf("That's the one I want!\n");
return 0;
}
(3)do while循环
? while和for循环都是入口条件循环,即在循环的每次迭代之前检查测试条件,所有有可能跟本不执行循环体中的内容。do while则是出口条件循环,即在循环的每次迭代之后才检查测试条件,保证了至少执行一次循环体。
do
statement
while (expression);
statement可以是一条简单语句或复合语句。典型do while伪代码如下
获得初值
do
{
处理该值
获得下一个值
} while(值满足测试条件);
# include <stdio.h>
int main(void)
{
const int secret_code = 13;
int code_entered;
do
{
printf("To enter the triskaidekaphobia therapy club.\n");
printf("please enter the secret code number: ");
scanf("%d", &code_entered);
} while (code_entered != secret_code);
printf("Congratulation! You are cured.\n");
return 0;
}
(4)循环控制语句:continue和break
? countinue语句会跳过本次迭代的剩余部分,并开始下一次的迭代。如果continue语句在嵌套循环内,则只会影响包含该语句的内层循环。
? 程序执行到循环中的break语句时,会终止包含它的循环,并继续执行下一阶段。如果break语句位于嵌套循环内,它只会影响包含它当前的循环。
(5)流程控制语句:goto
? goto语句使程序控制跳转至相应标签语句。冒号用于分隔标签和标签语句。标签语句遵循变量命名规则。标签语句可以出现在goto的前面或者后面。形式如下:
goto label;
label: statement
goto语句尽量少使用,C程序员可以接受一种goto的用法—出现问题时从一组嵌套循环中跳出(一条break语句只能跳出当前循环)。
五、流程控制(选择和分支)
(1)if语句
? if语句通常包含三部分,首先是if 关键字,然后是() 内的选择条件,最后是{} 中的选择体,如果选择体中只有一条语句则可以不使用{} 括起来。当选择条件为真,则执行{} 中的选择语句。if语句的通用形式如下:
if (expression)
statement
(2)if else语句
? if else语句通常包含四部分,首先是if else 关键字,然后是() 内的选择条件,最后是{} 中的选择体。当选择条件为真时,执行if 选择条件后的语句,当选择条件为假时,执行else 后的语句。if else 语句的通用形式如下:
if (expression)
statment1
else
statement2
(3)if -else if -else语句
? if - else if - else语句通常包含五部分,首先是if 、else if 和else 关键字,然后是() 内的选择条件,最后是{} 中的选择体。通用形式如下:
if (expression1)
statement1
else if (expression2)
statement2
else
statement3
这里要特别注意else和if的匹配问题,匹配的规则是,如果没有花括号{} ,else与离它最近的if匹配,除非最近的if被{} 括起来。
(4)switch break多重选择
? switch在圆括号() 中的测试表达式的值应该是一个整数值(包括char类型)。case标签必须是整数类型(包括char类型)的常量或整型常量表达式(即,表达式中只包含整型常量)。不能用变量作为case标签。swtich的构造如下:
switch(整型表达式)
{
case 常量1:
语句 <-可选
case 常量2:
语句 <-可选
default:
语句 <-可选
}
switch(expression)
{
case label1: statement1
case label2: statement2
default: statement3
}
break让程序离开switch语句,跳至switch语句后面的下一条语句开始执行。如果没有break语句,就会从匹配标签开始执行到swtich末尾。
六、字符输入/输出和输入验证
(1)缓冲区
? 无缓冲输入,是指在等待的程序可以立即使用用户输入的字符(无需按下enter键)。缓冲输入是指,用户的输入的字符都被收集在一个缓冲区的临时存储区域,按下enter键后,程序才能够使用用户输入的字符。缓冲分为两类:完全缓冲和行缓冲,完全缓冲是指当缓冲区域被填满之后才刷新缓冲区。行缓冲是指在出现换行符\n 时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下enter键后才刷新行缓冲区域。
? 通常,系统使用行缓冲输入,即当用户按下enter键后输入才被传送给程序。按下enter键也传送了一个换行符,编程时要注意处理这个换行符。
while (getchar() != '\n')
continue;
(2)文件和流
? 文件是存储器中储存信息的区域,通常文件都保存在某种永久存储器中。从概念上看,C程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流,这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都是通过流来完成。
? 文件的结尾,在C语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF (end of file的缩写)。scanf()函数检测到文件结尾时也将返回EOF 。通常,EOF 定义在stdio.h头文件中。
# define EOF (-1)
EOF 是一个值,标志着检测到文件的结尾,并不是在文件中能够找到的值。在大部分操作系统中,检测文件结束的一种方法是,在文件末尾放置一个特殊的字符标记文件结尾。在DOS系统中使用内嵌的**Ctrl + Z 字符来标记文件结尾,在UNIX系统中使用内嵌的Ctrl + D 字符来标记文件结尾。Windows把一行开始处的Ctrl + Z **识别为文件的结尾。
? C语言内置的三个标准流,C语言程序只要运行起来就默认打开了三个流,包括stdout、stdin、stderror 分别是标准输出流、标注输入流、标准错误输出流。
七、函数
(1)函数原型(函数声明)
? 自定义的函数必须要有函数声明,这是因为程序中首次遇到该函数的时候并不知道该函数的返回类型,所以必须通过前置声明(forward declaration)预先说明函数的返回类型。但是如果把函数的定义放在主函数main之前,就可以省略前置声明,因为编译器在执行到main之前就已经指导了该函数的所有信息。不过这不是复合C的标准风格,main只提供整个程序的框架,最好把main放在所有函数定义的前面(或者也可以将函数声明放置在main里面的变量声明处),另外,通常把函数放在其他文件中,所以前置声明必不可少。(这和python的风格不同,python允许函数出现在同一源文件的任何位置,并且无需前置声明就可任意调用,如果函数出现在其他文件中,那么就需要导入该文件模块。)
(2)带参数的函数
? ANSI C要求在每个变量前都声明函数类型,也就是说不能像普通函数变量那样使用同一类型的变量列表。
void dibs(int x, y, z);
void dibs(int x, int y, int z);
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,也可以省略形式参数变量名。如下:
void dibs(int, int, int);
void interchange(int*, int*);
其中void 表示函数的返回值类型,这个函数没有返回值所以是void 类型,dibs 表示函数名,(int x, int y, int z) 表示函数由三个形式参数。如果函数没有形式参数那么可以使用void 来表明没有参数列表,如下:
void pirnt_name(void);
包含指针的函数该如何声明示例:
int digit(double, int);
double* which(double*, double*)
(3)函数的返回值
? 使用return 语句的一个作用是,终止函数并把控制权返回给主调函数的下一条语句。声明函数的时候必须声明函数的类型,带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void 类型。函数的类型指的是函数返回值的类型,不是函数形式参数的类型。
? 如果函数的返回值的类型与声明的返回类型不匹配,返回值将被转换成函数声明的返回类型。
八、数组
(1)数组的定义
? 数组是按照顺序存储的一系列类型相同的值,整个数组有一个数组名,通过整数下标访问数组中单独的项或元素。C语言存在一个潜在的陷阱,考虑到影响执行速度,C编译器不会检查数组下标是否正确,因此需要保证数组下标不超过数组的所有索引。
char name[40];
方括号[] 表明创建了一个名为name的数组,**方括号中的数字表明数组中的元素个数,在C99新增变长数组之前,数组的大小必须是整型常量。**数组可以通过索引来进行访问,起始索引为0。也可以省略方括号中的数字,让编译器自动匹配数组的大小和初始化列表中的项数。数组的创建中应该使用常量表达式,而不能使用变量。一维数组在内存中是连续存放的,数组名存放着数组首元素的地址,数组中的各个元素地址从低到高依次增加。
(2)初始化数组
? 1、使用花括号{} 逐一对数组进行初始化,示例:
int power[8] = {1, 2, 4, 6, 8, 16, 32, 64};
当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中存储的都是垃圾值,但是,如果部分初始化数组,剩余元素就会被初始化为0。
? 2、指定初始化器,利用初始化器可以初始化指定的数组元素。可以在初始化列表中使用带方括号的下标指明待初始化的元素,例如:
int arr[6] = {[5] = 212};
举一个例子来说明初始化器的两个重要特性。
int days[12] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };
初始化器具有两个重要的特性,第一,如果指定初始化器后面还有更多的值,如该例中的初始化列表片段:[4] = 31, 30, 31 ,那么后面这些值将被用于初始化指定元素后面的值。第二,如果在此初始化指定的元素,那么最后的初始化将会取代之前的初始化。
(3)数组的赋值
? C不允许把数组作为一个单元赋值给另外一个数组,除初始化外也不允许使用花括号{} 列表形式的赋值。例如:
int oxen[5] = {5, 3, 2, 8};
int yaks[5];
yaks = oxen;
yaks[5] = {5, 3, 2, 8};
(4)指定数组大小
? 声明数组时,只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。并且数组的大小必须大于0。
(5)多维数组
? 创建一个多维数组示例:
float rain[5][12];
rain包含5个元素,5个元素中每个元素都是内涵12个float类型元素的数组。
多维数组的初始化,初始化时也可以省略内部的花括号,只保留最外层的一对花括号。只要保证初始化的个数正确,初始化的效果相同,但是如果初始化的数值不够,则按照先后顺序逐行进行初始化,直到用完所有值,后面没有初始化的元素被统一初始化为0。
? 创建多维数组时,第一个维度的参数可以省略,其余维度的参数不能省略,因为多维数组可以在初始化时根据初始化传入参数的数量,自动确定第一个维度的值。
(6)数组和指针
? 对C语言而言,不能够把整个数组作为参数传递给函数,但是可以传递数组的地址。数组名是数组首元素的地址,也就是说直接使用数组名可以表示该数组的首元素的地址,如果flizny是一个数组,那么下面的语句成立:
flizny == &flizny[0];
利用这种特性,可以用另一种方式来访问数组:
flizny + 2 == &flizny[2]
flizny[2] == *(flizny + 2)
? 想要让一个数组作为形式参数,那么应该如何定义呢?记住,数组名是该数组首元素的地址,所有实际参数是一个存储数组首元素地址的指针,应该把它赋给一个指针形式参数,即该形参指向一个指针。
int sum(int ar);
int sum(int* ar);
int sum(int ar[]);
int sum(int* );
int sum(int []);
只有在函数原型或者函数定义头中,可以用int ar[] 来代替int* ar 。另外需要注意的一点是,如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const 。一般来说,如果编写的函数需要修改数组,在声明数组形参时则不使用const ;如果编写的函数不用修改数组,那么在声明数组形参时最好使用const 。例如:
int sum(const int ar[], int n);
int sum(int ar[], int n);
int sum(const int [], int);
int sum(int [], int);
数组和指针初始化字符串有什么异同呢?
总之,初始化数组是把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。两者的主要区别是:数组名是常量,而指针则是变量,这一点的体现是数组名不能使用单目运算符++ ,-- 等。
(7)变长数组
? C99新增了变长数组(variable-length array, VLA),允许使用变量表述数组的维度。变长数组有一些限制。变长数组必须是自动储存类别,这意味着无论在函数中声明还是作为函数形式参数声明,都不能使用static或extern储存类别说明符。注意变长数组不能改变大小,变长数组中的”变“不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小则保持不变,这里的”变“指的是,在创建数组时,可以使用变量指定数组的维度。例如:
int sum2d(int rows, int cols, int ar[rows][cols]);
int sum2d(int, int, int [*][*]);
如果省略了维度参数名,那么必须要用星号* 来代替省略的维度。
(8)复合字面量
? 为了给带数组形参的函数,传递等价的数组常量,C99新增了复合字面量(compound literal)。字面量是除符号常量外的常量。**对于数组,复合字面量类似数组初始化列表,前面使用括号括起来的类型名。**例如:
int diva[2] = {10, 20};
(int [2]){10, 20};
int *pt1;
pt1 = (int [2]){10, 20};
记住,复合字面量是提供临时需要值的一种手段,复合字面量具有块作用域,一旦离开定义复合字面量的块,程序将无法保证该字面量仍然存在。
九、指针
(1)指针的定义
? 从根本上看,指针是一个值为内存地址的变量(或数据对象)。指针变量的值是地址。要创建指针变量,先要声明指针变量的类型。
? 指针的声明,在声明指针时,必须指定指针所指向变量的类型,因为不同类型的变量占用不同的存储空间,一些指针操作要求知道对象的大小。另外,程序必须知道存储在指定位置上的数据类型。指针声明示例:
int *pi;
char *pc;
float *pf, *pg;
类型说明符表明了指针所指向对象的类型,星号* 表明声明的变量是一个指针。* 和指针名之间的空格可有可无,通常,程序员在声明时使用空格,在解引用变量时省略空格。指针的类型决定了指针运算时加减的字节数。
(2)一元运算符& 和*
查找地址:&运算符
? 一元运算符&给出变量的存储地址。如果epoch是变量名,那么&epoch是变量的地址。
间接运算符:*
? 使用间接运算符* 能够取出存在指定指针位置的值,该运算符有时也被称为解引用运算符。使用方法如下:
val = *ptr;
prt = &bah;
val = *ptr;
(3)什么时候使用指针?
? 一般而言,可以把变量相关的两类信息传递给函数,有两种调用形式:
function1(x);
function2(&x);
第一种形式要求函数定义中的形式参数必须是一个和x的类型相同的变量,第二种形式要求函数定义中的形式参数必须是一个指向正确类型变量的指针。如果要计算或处理值,那么使用第一种形式的函数调用;如果要在被调用函数中改变主调函数的变量,则使用第二种形式的函数调用。只有程序需要在函数中改变变量的值时,才会传递指针。对数组而言,别无选择必须传递指针,因为这样做的效率更高。
(4)指针变量的基本操作
操作名 | 操作注意事项 |
---|
赋值 | 可以把地址赋值给指针 | 解引用 | * 运算符给出指针指向地址上存储的值 | 取址 | 和所有变量一样,指针变量也有自己的地址,可以用另一个指针来表示一个指针变量的地址。 | 指针和整数相加减 | 无论是加、减、递增还是递减,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与地址相加。假设ptr1 指向urn 的第一个元素,因此ptr1 + 4 与&urn[4] 等价。指针加上一个整数或者递增指针,指针指向的值以所指向对象的大小未单位改变。 | 比较 | 使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。 | 指针-指针 | 同类型的指针与指针进行减法运算结果是两个指针之间的元素个数 |
(5)const 修饰指针
? 虽然使用# define 指令可以创建类似功能的符号常量,但是const 的用法更加灵活,可以创建const 数组,const 指针和指向const 的指针。
-
指向const 的指针,不能用于修改值: double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double *pd = rates;
*pd = 29.89;
rates[0] = 29.89;
pd++;
指向const 的指针通常存在于函数形参中,表明该函数不会使用指针改变数据。
- 1、指向
const 的指针可以指向为const 数据或非const 数据;2、普通指针只能指向非const 数据: double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
const double *pc;
double *pnc;
pc = rates;
pc = locked;
pnc = rates;
pnc = locked;
? 这条规则非常合理,否则,通过指针就能修改const 数组中的值。因此,对函数的形参使用const 不仅能够保护数据,还能让函数处理const 数组。 -
声明一个不能指向别处的指针,注意const 的使用位置: double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
double * const pc = rates;
pc = &rates[2];
*pc = 99.99;
-
在创建指针时,可以使用两次const ,该指针既不能更改它所指向的地址,也不能修改指向地址上的值: double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * const pc = rates;
pc = &rates[2];
*pc = 99.99;
简而言之,把const 放在* 左侧任意位置,限定了指针指向的数据不能改变;const 放在* 右侧,限定了指针本身不能改变。
(6)二级指针
? 指针变量也是一种变量,也会占用存储空间,也可以使用& 获取它的地址,C语言不限制指针的级数,在定义指针变量的时候就得增加一个* 号,同理在解引用时也得按照指针的级数来确定* 的个数。示例:
int a = 100;
int* p = &a;
int** pp = &p;
printf("%d\n", *p);
printf("%d\n", **pp);
在实际的开发过程中经常会用到一级、二级指针但是高级指针基本用不到。
(7)指针数组(存放指针的数组)
如何声明一个指针变量指向一个二维数组,如,zippo:
int(* pz)[2];
int * pax[2];
圆括号() 的优先级要高于* ,所以* 先和pz结合,因此声明了一个指向数组(内涵两个int类型的值)的指针。
由于[] 优先级高,先与pax结合,所以pax成为一个内含两个元素的数组,然后* 表示pax数组内含两个指针。
假设junk是一个3X4的数组,要声明一个指向数组(内含4个int类型值)的指针,可以这样声明函数的参数。
void somefunction(int (*pt)[4]);
另外,当且仅当pz是另一个函数的形式参数时,可以这样声明:
void somefunction(int pt[][4]);
一般而言,声明一个指向N维数组的指针,只能省略左边方括号中的值。
指针数组的类型是什么?
int* arr1[10];
char** arr2[4];
char*** arr3[5];
(8)数组指针(指向数组的指针)
? 数组指针是一种指针,是能够指向数组的一种指针,指向数组的地址(注意不是指向数组的首元素地址,需要将这两者区分开来),如何定义一个数组指针呢?
? 数组指针的定义,由于* 的优先级小于[] ,那么需要使用() 包含* ,后面再接上[] 表示这个是数组,合起来表示一个数组指针。示例如下:
int arr[10] = { 0 };
int (*pd)[10] = &arr;
double* ch[5];
double* (*pc)[5] = &ch;
数组指针和数组首元素地址的区别:&arr 表示数组首元素的地址,而arr 表示数组的地址,数组的地址+1 会跳过整个数组的大小。
数组指针的使用,数组指针是指向整个数组的,对数组指针进行解引用相当于获得了数组名,然后就可以进行遍历数组的操作,示例:
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int (*pa)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", *((*pa) + i));
printf("%d\n", (*pa)[i]);
}
数组指针多用于处理二维数组,对二维数组而言,数组名是数组首元素的地址,首元素是指一个一维数组,对这个一维数组而言,首元素地址为数组的一个元素的地址,所以二维数组名是第一个一维数组的地址,同时也是数组第一个元素的地址。
int arr[10] = { 0 };
int (*ptr)[10] = &arr;
(9)函数指针
? 函数指针是存放函数地址的指针,&函数名 获得了函数的地址,注意两点:&函数名 == 函数名 ,&数组名 != 数组名 。函数名存放的就是函数的地址,&函数名也是得到函数的地址。而数组名存放的是数组首元素的地址,注意区别。
? 函数指针的定义,函数指针的定义需要说明函数的参数类型和函数的返回值类型,定义和数组指针类似,由于* 号优先级不高,所以需要() 括起来。示例:
int Add(int x, int y);
int (*ptr)(int, int) = &Add;
ptr = Add;
? 如果省略的* 前面的() 那么情况则会完全不同
int * ptr(int, int);
? 函数指针的解引用,对函数指针进行解引用就可以获得函数名,然后可以利用() 进行函数的调用。实际上,不需要对函数指针进行解引用,函数指针和函数名实际上是等价的,因为函数名本身就是指针。
printf("%d\n", Add(1, 3));
printf("%d\n", (*ptr)(1, 3));
printf("%d\n", ptr(1, 3));
(10)函数指针数组
? 函数指针数组是存放函数指针的数组,函数指针数组内可以存放同类型的函数指针。函数指针数组的定义:
int ADD(int x, int y)
{
return x + y;
}
int SUB(int x, int y)
{
return x - y;
}
int (*pfArr[2])(int, int) = { ADD, SUB };
函数指针数组的作用是用作转移表,方便多个相似函数的调用。
十、存储类别、链接和内存管理
(1)作用域
? C语言的作用域包括:块作用域、函数作用域、函数原型作用域、文件作用域。
- 块作用域,在块作用域中声明的变量,只能在块作用域中使用,如何区分一个块?一个块是一对
{} 括起来的代码区域。 - 函数作用域,函数作用域仅仅适用于goto语句的标签。
- 函数原型作用域,函数原型作用域的范围是从函数形参的定义处到原型声明的结束处。
- 文件作用域,具有文件作用域的变量,从它的定义处到该定义所在的文件末尾均可见,文件作用域变量又称为全局变量。
(2)链接
? C语言的变量有三种连接属性:外部链接、内部链接和无连接。
? 具有块作用域、函数作用域或函数原型作用域的变量都是无连接属性,这意味着这些变量属于定义他们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或者内部链接。外部链接的变量可以在多文件程序中使用,而内部链接的变量只能在一个文件中使用。
(3)存储期
? C对象有四种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
? 文件作用域变量具有静态存储期,块作用域变量具有自动存储期。介绍五种不同的存储类别:
1、存储类别关键字auto\register\static\extern
? atuo 的作用是声明一个变量的存储类别是自动存储类别。自动变量属于自动存储类别,具有自动存储期、块作用域且无连接。程序进入该变量声明所在的块时变量存在,程序退出该块的时候变量销毁。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚表达意图(例如,为了表明有意覆盖一个外部定义的变量,或者强调不要把该变量改成其他存储类别),可以显示使用关键字auto 。
? register 的作用是声明一个变量的存储类别是寄存器存储。
? static 的作用是声明一个静态变量,静态的意思是该变量在内存中原地不动,并不是说它的值保持不变。
? extern 的作用是为了指出函数使用了外部变量,可以在该变量前加上extern 声明,外部变量只能初始化一次,并且只能在定义该变量时进行。如果一个源代码文件使用的外部变量或者函数定义在另一个源代码文件中,则必须使用extern 在该文件中声明该变量。
2、存储类别关键字_Thread_local\typedef
(4)随机值函数rand\srand\time
? 如何正确使用随机函数rand ,rand 在使用前需要设置随机数种子,通过srand 将随机数种子设置为time 当前的时间,那么每次生成随机数的序列就不同,实现了真正的随机。
功能 | 函数 | 头文件 | 函数细节 |
---|
生成随机数 | rand | stdlib.h | 返回一个随机数,范围在0~RAND_MAX之间 | 设置随机数种子 | srand | stdlib.h | 设置随机数种子,传入设置的种子 | 获得当前时间 | time | time.h | 传入用于保存结果的指针,也可以传入空指针,返回当前的时间 |
(5)动态内存分配函数malloc\calloc\realloc\free
功能 | 函数 | 头文件 | 函数细节 |
---|
开辟内存空间 | malloc | stdlib.h | 参数接受开辟空间的字节数,返回开辟空间的指针,类型是void*,可以通 过强制类型转换来更改指针类型,如果开辟空间失败则返回NULL指针 | 开辟内存空间 | calloc | stdlib.h | 参数接受开辟空间的元素个数和每个元素的字节数,开辟内存空间并且全 部初始化为0,返回开辟空间的指针,开辟失败返回NULL | 重新开辟内存空间 | realloc | stdlib.h | 参数接受已经开辟过空间的指针,重新开辟空间的字节大小,返回类型是void*, 如果开辟失败则返回NULL | 释放空间 | free | stdlib.h | 参数接受已开辟空间的指针,并且释放指针指向的内存空间。free使用的函 数指针不必与malloc的指针变量一致,但是两个指针必须指向同一内存空间。 |
内存泄漏(memory leak)是指没有释放已经分配了的内存空间,却丢失了指向该空间的指针,导致无法再次访问该片内存,这就是内存泄漏。
(6)限定类型关键字const\volatile\restrict\_Atomic
? const 的作用是,限定变量的属性是只读,不能被更改,只能被初始化。
? volatile 的作用是,限定变量的属性是可以更改的,限定的数据处理被当前的程序修改之外还可以被其他的进程修改,主要用于在线程中处理变量的情况,主要用于并发程序设计。
? restrict 的作用是,限定指针是访问数据对象的唯一且初始的方式,只能够用于修饰指针对象。方便编译器进行优化。
十一、文件与IO
(1)文件
? C语言提供两种文件模式:文本模式和二进制模式。所有的文件都以二进制形式进行存储,但是,如果文件最初使用二进制编码的字符(如Unicode)表示文本,该文件就是文本文件。如果文件中的二进制代表机器语言代码或者数值或者图片或者音乐编码,那么该文件就是二进制文件,其中包含二进制内容。如果需要不损失精度的前提下保存或恢复数据,请使用二进制模式以及fread()和fwrite()函数。
(2)IO函数
? 缓冲文件系统中的关键概念是文件指针FILE* ,用于指向一个文件对象结构体,其中包含操作发文件IO函数所用的缓冲区的位置,对文件指针进行操作,就可进行文件的读写。FILE* 指针被定义在stdio.h 库中。
功能 | 函数 | 库 | 函数细节 |
---|
打开文件对象 | fopen | stdio.h | 打开文件返回文件指针,如果打开失败返回NULL | 关闭文件对象 | fclose | stdio.h | 关闭文件指针所指向的文件对象,关闭成功则返回0,否则返回EOF, 关闭文件时会刷文件新缓冲区 | 字符输入函数 | fgetc / getc | stdio.h | 返回从文件对象获取的字符,成功返回字符,失败返回EOF | 字符输出函数 | fputc / putc | stdio.h | 将字符写入文件对象,成功写入的字符,失败返回EOF | 字符串输入函数 | fgets | stdio.h | 从流对象获取字符串,需要指定字符串的长度,并且会在字符串后面自动添加\0 所以只能存放指定长度-1长度字符串,获取失败返回NULL,成功返回str的指针 | 字符串输出函数 | fputs | stdio.h | 输出字符串到流对象, | 格式化输入函数 | fscanf | stdio.h | 从流对象获得格式化输入,如果接收成功则返回1,失败则返回0 | 格式化输出函数 | fprintf | stdio.h | 写入格式化输出到流对象,如果成功输出则返回输出的字符数(包含\0 ), 失败则返回一个负数 | 二进制输入函数 | fread | stdio.h | 只适用于文件,返回成功读取的项数 | 二进制输出函数 | fwrite | stdio.h | 只适用于文件,返回成功写入的项数 |
fopen打开的模式如下:
(3)文件指针操作函数
功能 | 函数 | 头文件 | 函数细节 |
---|
文件指针返回文件开始处 | rewind | stdio.h | 重置文件指针的位置 | 指定文件的指针的位置 | fseek | stdio.h | 通过偏移量和起始量,获得文件指针的位置,如果正常返回值是0, 出现错误返回值是0,偏移量应该是一个long类型是数据,比如0L,1L | 返回文件指针的位置 | ftell | stdio.h | 返回文件指针的位置 | 获得文件指针的位置 | fgetpos | stdio.h | | 设置文件指针的位置 | fsetpof | stdio.h | |
fseek 可以使用三种文件位置的起始量
(4)检测文件错误函数
功能 | 函数 | 头文件 | 函数细节 |
---|
判断是否因为文件末尾EOF导致 | feof | stdio.h | 如果由于EOF导致错误,则返回非零值,否则返回0 | 判断是否因为读写错误导致 | ferror | stdio.h | 如果由于读写错误导致,则返回非零值,否则返回0 |
十二、其他数据类型
(1)结构体struct
? 结构体的声明,结构体的声明和定义结构体变量可以组合成一个步骤。
struct tag
{
member-list;
} variable-list;
? 结构体的初始化,可以使用{} 来对结构体进行初始化,例如:
struct tag a ={ initial-list};
结构体具有指定初始化器,其语法和数组的指定初始化器类似,但是,结构体的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素。例如,只初始化book结构体的value成员。另外,对特定成员的最后一次赋值才是它实际获得的值。
struct book b = {.value = 10.99};
? 结构体的访问,有两种方式,第一种使用. 通过结构体变量来对结构体成员进行访问,第二种使用-> ,通过结构体指针来对成员进行访问。
(2)结构体的一些特性
? 需要注意,必须使用&运算符来获取结构体的地址。和数组名不同,结构体名不是其地址名的别名。
? 对于需要结构体传参的时候,尽可能传递结构体的地址,而不是直接传递结构体变量,因为直接传递结构体变量会拷贝一份结构体,造成大量的内存消耗。
? C语言允许将一个结构体直接赋值给另一个结构体,但是数组却不能这样做,也就是说如果n_data和o_data是相同类型的结构体,那么可以这样做:
n_data = o_data;
? 结构体复合字面量,可以将结构体复合字面量传递给相应的结构体,作为结构体的值,给结构体进行赋值。语法是吧类型名放在圆括号() 里,后面紧跟一个用花括号{} 括起来的初始化列表。例如:
struct book = {
char title[30];
char author[30];
float price;
}
struct book ABook;
ABook = (struct book){"The Idiot", "Fyodor Dostoyevsky", 6.99};
(3)联合体union
? 联合体是一种数据类型,它能在同一个内存空间中存储不同的数据类型(不是同时存储)。声明的联合体只能存储其中一种类型的值,这与结构体不同。联合体的典型用法是,设计一种表来存储既无规律,也事先不知道顺序的混合类型。使用联合体的数组,其中的联合体都大小相等,每个联合体都可以存储各种数据类型。
? 联合体的初始化,有三种方法:1、把一个联合体初始化为另一个同类型的联合体。2、默认初始化联合的第一个元素。3、使用指定初始化器进行初始化。示例:
union hold{
int digit;
double bigft;
char letter;
};
union hold valA;
valA.letter = 'R';
union hold valB = valA;
union hold valC = {88};
union hold valD = {.bigft=118.2};
? 联合体的访问,有两种方式,第一种使用. 通过联合体变量来对结构体成员进行访问,第二种使用-> ,通过联合体指针来对成员进行访问。
(4)枚举enum
? 实际上枚举enum 常量是int类型,因此,只要能使用int类型的地方就能够使用枚举类型。枚举类型的目的是提高程序的可读性。它的语法与结构体类似。枚举类型中常量被称为枚举符
? 枚举的声明
enum spectrum{
red,
orange,
yellow,
};
enum spectrum color;
? 枚举常量的赋值,在枚举的声明中,可以为枚举常量指定整数值。如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。
(5)typedef
? typedef关键字可以用于建立C标准类型的别名或缩写。
? 利用typedef可以为某一类型自定义名称。typedef由编译器运行,不是预处理语句。typedef定义的变量的作用域取决于typedef语句的位置,如果定义在函数内部,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。举个例子:
typedef unsigned char BYTE;
BYTE x, y[10], *z;
? 还可以将typedef用于结构体,例如:
typedef struct complex{
float real;
float imag;
} COMPLEX;
? 使用typedef来命名一个结构体的时候,还能够省略该结构体的标签
typedef struct {
double x;
double y;
} rect;
? 使用typedef时需要记住,typedef并没有创建任何新的类型,它只是为某个已存在的类型新增加了一个方便使用的标签。
? typedef还可以用于函数指针,例如:
typedef void(* V_FP_CHARP)(char* );
? 如何使用自定义的函数指针类型呢?例如:
V_FP_CHARP fp;
void(*fp)(char* );
(6)一些复杂的声明
关键要理解*、()、[] 的优先级。记住三条规则:
? 1、()、[] 的优先级高于* 。
? 2、()、[] 的优先级相同。
? 3、()、[] 都是从左往右结合。
十三、数据的存储格式
? 关于进制的问题:
? 每个八进制(octal)位对于3个二进制位,每个十六进制(hex)位对应4个二进制位。
(1)整数的存储格式
? 数据是以二进制序列进行存储的,一个字节byte有8个位bit,每一bit都能存放一个二进制数。例如int需要四个字节来存储,那么一个int类型的变量所占的字节为4*8=32 位。无符号的整数所有的bit都用于存放二进制数。
? 有符号的整数,最高位用于存放符号位1 表示是负数,0 表示是正数,负数在内存中的存储是以二进制补码的形式(two’s complement),具体的做法就是反转除了符号位的每一位,然后再+1。如果需要从补码获得原码也是相同的做法。
00000000 00000000 00000000 00000001
10000000 00000000 00000000 00000001
11111111 11111111 11111111 11111110
11111111 11111111 11111111 11111111
(2)浮点数的存储
? 见C语言细节.md
(3)位运算与掩码
? 按位与运算通常用于掩码mask,所谓掩码就是指一些设置开(1)或关(0)的位组合。
(4)位字段
位字段bit field通过一个结构体来声明,该结构体声明位每个字段提供标签,并且确定该字段的宽度。
C语言有两种访问位的方法,第一种是通过按位运算符,另一种方法是在结构中创建位字段。
十四、C预处理器
? 预处理指令从# 开始运行,到后面第一个换行符为止。也就是说,预处理的指令长度仅限于一行。
(1)符号常量#define
? #define 和其他预处理指令一样,以# 号作为一行的开始。指令可以出现在源文件的任何位置,其定义从指令出现的地方到该文件末尾有效。
每行#define 都由三部分组成,第一部分是#define 指令本身。第二部分是选定的缩写,也成为宏。宏的名称中不允许有空格,而且必须遵守C语言的命名规则:只能使用字符、数字和下划线_ ,而且首字符不能是数字。第三部分是替换体。一旦预处理器在程序中找到宏的实例后,就会用替代体来替代该宏。从宏变成最终替代文本的过程称为宏展开。在预处理阶段,预处理器不做计算,不对表达式进行求值,它只是进行宏的替换。
(2)在#define 中使用参数
? 在#define 中使用参数可以创建外形和作用与函数类似的类函数宏。
宏在预处理阶段完成的是替换的工作,所以必要时要使用足够多的圆括号() 来保证运算和结合的正确顺序。并且应该避免使用++x 等作为宏的参数,因为这些行为是未定义的,一般而言,不要在宏中使用递增或者递减运算符。
? 用宏创建字符串:# 运算符,C语言允许在字符串中包含宏参数,在类函数宏的替换体中,# 号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏参数,那么#x 就是转换位字符串"x" 的形参名,这个过程称为字符串化。示例:
#define PSQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)))
? 预处理器黏合剂:## 运算符,## 运算符可以用于类函数宏的替换部分。## 的作用是把两个记号组合成一个记号。
#include <stdio.h>
#define XNAME(n) x##n
#define PRINT_XN(n) printf("x"#n"=%d\n", x##n)
int main(void)
{
int XNAME(1) = 14;
int XNAME(2) = 20;
int x3 = 30;
PRINT_XN(1);
PRINT_XN(2);
PRINT_XN(3);
return 0;
}
? 变参宏:... 和__VA_ARGS__ ,通过把宏参数列表中最后的参数写成省略号... 来实现变参的功能,__VA_ARGS__ 可以用在替换部分中,表示省略号代表什么。例如:
#include <stdio.h>
#include <math.h>
#define PR(X,...) printf("Message"#X":"__VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x=%g\n", x);
PR(2, "x=%.2f, y=%.4f\n", x, y);
return 0;
}
(3)文件包含#include
? 当预处理器发现#include 指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换文件中的#include 指令。这样做相当于把被包含的文件的全部内容输入到源文件#include 指令所在的位置。#include 有两种形式:
#include <stdio.h>
#include "mystuff.h"
在UNIX系统中,尖括号<> 告诉预处理器在标准系统目录中查找该文件。所以使用<> 一般是包含系统自带的头文件。双引号"" 告诉预处理器首先在当前工程目录中(或文件名中指定的其他目录)查找该文件,如果未找到再到标准系统目录中进行查找。例如:
#include <stdio.h>
#include "hot.h"
#include "/usr/bin/cold.h"
(4)使用头文件
? C语言中习惯用.h 后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。在大部分情况下,头文件的内容是编译器生成最终代码时所需要的信息,而不是添加到最终代码中的材料。
? 头文件中最常用的形式如下:包含符号常量、宏函数、函数声明、结构模板定义、类型定义。
(5)其他预编译指令
? #undef 用于取消已经定义的#define 指令
#define LIMIT 400
#undef LIMIT
移除上面的定义,现在可以将LIMIT重新定义为一个新值。即使原来没有定义LIMIT,取消LIMIT的定义仍然有效。如果想使用一个名称,有不确定之前是否定义过,为了安全起见可以使用#undef 指令取消该符号的定义。需要注意,#define 宏的作用域从它在文件中的声明处开始,直到用#undef 指令取消宏定义处为止,延伸至文件末尾。
? #defined 用于判断标识符是否是#define 定义过的,如果是定义过的则返回1,否则返回0。这种新方法常常与#elif 一起使用。
(6)条件编译
? 1、#ifdef、#else、#endif
? #ifdef、#else、#endif 的逻辑和if、else 语句很相似,只不过#ifdef 需要#endif 来标志预处理语句的结束。
#ifdef MAVIS
#include "horse.h"
#define STABLES 5
#else
#include "cow.h"
#define STABLES 15
#endif
? 2、#ifndef、#else、#endif
? #ifndef 指令与#ifdef 指令的用法类似,也可以和#else、#endif 一起使用,但是它们的逻辑相反。#ifndef 可以判断后面的标识符是否是未定义的,常用于定义之前未定义的标识符常量。
#ifndef SIZE
#define SIZE 100
#endif
#ifndef 还可以用于防止多次包含同一个文件,例如在things.h 头文件中:
#ifndef THINGS_H_
#define THINGS_H_
...
#endif
防止头文件多次包含还可以使用**#pragma once **语句,在头文件的第一行添加该语句,可以使头文件只包含一次。
#pragma once
? 3、#if、#elif
? #if 指令很像C语言中的if ,#if 后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用C的关系运算符和逻辑运算符。例如:
#if SYS==1
#include "horse.h"
#elif SYS==2
#include "cat.h"
#else
#include "dog.h"
#endif
? #if、#elif 和#defined 一起使用,可以达到#ifdef 的效果,例如:
#if defined(IBMPC)
#include "ibmpc.h"
#elif defined(VAX)
#inlcude "vax.h"
#else
#include "genreal.h"
#endif
(7)预定义宏和__func__
? C99标准提供一个名为__func__ 的预定义标识符,它展开为一个代表函数名的字符串(该函数包含该标识符)。那么__func__ 必须具有函数作用域,而从本质上看宏具有文件作用域。因此,**__func__ 是C语言的预定义标识符而不是预定义宏。**简单来说,__func__ 预定义标识符代表函数名的字符串,__func__ 在不同的函数内部调用时,其代表该作用域下的函数名称的字符串。
(8)泛型选择_Generic
? _Generic 是C11的关键字。_Generic 后面的圆括号中包含多个用逗号分隔的项。第一项是一个表达式,后面的项都由一个类型、一个冒号和一个值组成,例如float:1 。第一个项的类型匹配哪个标签,整个表达式的值就是该标签后面的值。例如:
_Generic(x, int:0, float:1, double:2, default:3)
假设上面表达式中的x是int类型的变量,x的类型匹配int:0 的标签,那么整个表达式的值就为0。如果没有与类型匹配的标签,那么整个表达式的值就为默认值default:3 。泛型选择语句与switch 语句类似,只是前者用表达式的类型匹配标签,后者用表达式的值匹配标签。
十五、C库
? 头文件包含了本系列所有函数的声明,这样直接包含头文件,就可以省略在每次使用函数都要包含函数原型声明的复杂工作。例如
(1)ctype.h
(2)math.h
(3)stdio.h
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FPSUB3Pn-1655802386023)(D:\Study_Data\Markdown_Note\figure\image-20220621143125665.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CVZo8FYS-1655802386024)(D:\Study_Data\Markdown_Note\figure\image-20220621143249782.png)]
(4)stdlib.h
(5)stdlib.h中的exit()和atexit()
? 1、atexit() 函数的用法
? 这个函数使用函数指针。要使用atexit() 函数,秩序把退出时要调用的函数地址传递给atexit() 即可。然后,atexit() 注册函数列表中的函数,当调用eixt() 时就会执行这些函数(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)。需要注意一点是即使没有显示调用exit() 函数,程序仍会在退出时执行ateixt() 注册的函数,这是因为main() 结束时会隐式调用exit() 。
? 2、exit() 函数的用法
? exit() 执行完atexit() 指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数创建的零时文件。然后exit() 把控制权返回主机环境。UNIX程序使用0表示成功终止,用1表示终止失败。C定义了一个名为EXIT_FAILURE 和EXIT_SUCCESS 表示终止失败和成功终止。
不过,exit() 函数也接受0表示成功终止,接受1表示终止失败。在非递归的main() 函数中使用exit() 函数等价于使用return 。尽管如此,在main() 函数以外的函数中使用exit() 也会终止整个程序。
(6)assert.h
? assert() 宏接受一个整型表达式作为参数,如果表达式求值为假,assert() 宏就在标准错误流(stderr)中写入一条错误信息,并且调用abort() 函数终止程序(abort() 函数的原型在stdlib.h 头文件中)。
? assert() 拥有无需更改代码就能开启或者关闭的机制。如果认为已经排除了程序的BUG,就可以把下面的宏定义写在包含assert.h 的前面:
#define NDEBUG
并重新编译程序,这样编译器就会禁止文件中的所有assert() 语句。如果程序又出现问题,那么可以移除这条语句,重新编译程序这样又启用了assert() 语句。
? 与assert 相类似的断言关键字还有_Stattic_assert 。两者的区别在于assert 可以导致正在运行的程序中止,而_Stattic_assert 可以导致程序无法通过编译,并且打印错误信息。
Reference
《C Primer Plus》
|